Golang 反射操作整理

前言

反射是什么? 我们平常也是经常用到, 而且这名词都用烂了, 这里就不再详细介绍了.

简单说, 就是有一个不知道是什么类型的变量, 通过反射可以获取其类型, 并可操作属性和方法.

反射的用途一般是用作生成工具方法, 比如你需要一个ToString方法, 要将变量转为字符串类型, 如果没有反射, 就需要写: ToStringInt, ToStringBool…等等, 每一个类型都要加一个方法. 而有了反射, 只需要一个ToString方法, 不管是什么类型的变量, 都扔给他就好啦.

对于PHP这种弱类型的语言来说, 如果要调用变量$a$b方法, 只需要$a->$b()即可. 而对于Golang这种强类型的语言就不能这么随意了. 故下面简单介绍一下Golang中反射的应用.

希望看完反射的用法之后, 至少以后再看相关代码不至于一脸懵逼. 甚至于需要的时候还能自己手撸一套.

使用

Golang中反射的操作定义在包reflect中. 此包中主要包括以下两个对象:

  • reflect.Type 用于获取变量的类型信息
  • reflect.Value 用于对变量的值进行操作

官方文档地址: reflect.Type reflect.Value

我们在反射中的使用, 也是基于这两个对象的.

对于反射的使用来说, 其实我们在平常使用中, 主要也就用到下面这几种操作, 大部分复杂的操作百年难得一用:

  • 变量类型和属性的操作
  • 变量方法的操作

下面就基于这两种操作进行简单演示.

变量类型和属性的操作

获取属性信息

反射中, 类型信息通过reflect.Type对象获取.

获取类型

u := struct {
  name    string
}{}
// 获取反射对象信息. 既 reflect.Type 对象
typeOf := reflect.TypeOf(u)
fmt.Println(typeOf)
// 变量的类型名 User
fmt.Println(typeOf.Name())
// 获取变量的底层类型. 
// 基础类型的底层类型就是它本身, 比如 int
// 而所有的自定义结构体, 底层类型都是 struct
// 所有的指针, 底层类型都是 ptr
// golang 的所有底层类型定义在 reflect/type.go 文件中. 
// 可通过 reflect.Array 常量进行定位
fmt.Println(typeOf.Kind())

但是, 别高兴的太早, 如果将变量u换成一个指针: u := &User{}. 使用上述方法就拿不到变量的类型了. 因为变量内容存储的是地址, 所以需要对该地址进行取值操作. 反射包中的取值操作方法为: reflect.TypeOf(u).Elem(). 拿到值后, 就都一样啦.

不止是指针, 包括: Array, Chan, Map, Ptr, Slice等, 其实存储的都是地址, 故都需要进行取值操作.

注意, 这里的底层类型Kind太有用了, 在通过反射处理的时候, 用户自定义类型太多了根本判断不过来, 但是其底层类型Kind一共就十几个, 相同底层类型的结构就可以使用相同的处理方式了.

结构体

type User struct {
  Gender int
}
u := struct {
  name    string
  Age     int
  Address struct {
    City    string
    Country string
  } `json:"address"`
  User
}{}

/* 获取反射对象信息 */
typeOf := reflect.TypeOf(u)
// 结构体字段的数量
fmt.Println(typeOf.NumField())
// 获取第0个字段的信息, 返回 StructField 对象 (此对象下方有说明)
// 可拿到字段的 Name 等信息, 包括字段的 Type 对象
fmt.Println(typeOf.Field(0))
// 根据变量名称获取字段
fmt.Println(typeOf.FieldByName("name"))
// 获取第2个结构提的第0个元素
fmt.Println(typeOf.FieldByIndex([]int{2, 0}))

/* StructField 字段对象内容 */
structField := typeOf.Field(2)
// 字段名
fmt.Println(structField.Name)
// 字段的可访问的包名
// 大写字母打头的公共字段, 都可以访问, 故此值为空
fmt.Println(structField.PkgPath)
// reflect.Type 对象
fmt.Println(structField.Type)
// 字段的标记字符串, 就是后面跟着的 `` 字符串
// 返回 StructTag 对象, 下方有说明
fmt.Println(structField.Tag)
// 字段在结构体的内存结构中偏移量, 字节
fmt.Println(structField.Offset)
// 字段在结构体中的索引
fmt.Println(structField.Index)
// 匿名字段. 结构体中的 Gender 就属于匿名字段
fmt.Println(structField.Anonymous)

/* StructTag 标签内容 */
tag := structField.Tag
// 获取指定名称的标签值, 若不存在, 返回空字符串
fmt.Println(tag.Get("json"))
// 与 Get 方法区别是, 第二个参数会返回若标签是否存在存在
// 有些标签的空字符串与未定义行为不同时, 可使用此方法获取
fmt.Println(tag.Lookup("json"))

数组

切片的底层类型为Slice, 但不同切片中存储的对象类型是不一样的.

说白了, 数组其实就是一个指向首地址的指针嘛. 故要想获取数组元素的内容, 做一次取值操作就可以啦.

l := []int{1, 2}

typeOf := reflect.TypeOf(l)
// 空, 这里为什么空上面说过了, 数组是一个指针
fmt.Println(typeOf.Name())
// slice
fmt.Println(typeOf.Kind())
// 获取数组元素的类型
fmt.Println(typeOf.Elem().Kind())
fmt.Println(typeOf.Elem().Name())

如果数组中存放的是结构体, 在用作结构体处理就好啦

map

m := map[string]int{
  "a": 1,
}
typeOf := reflect.TypeOf(m)
// map 不使用取值也可以打印名字 map[string]int 不懂
fmt.Println(typeOf.Name())
// 对象底层类型. map
fmt.Println(typeOf.Kind())
// 获取 map 的 key 的类型
fmt.Println(typeOf.Key().Kind())
// 获取 map value 的类型
fmt.Println(typeOf.Elem().Kind())

获取属性值

反射中, 对于值的操作, 都是通过reflect.Value对象实现的, 此对象通过reflect.ValueOf获取.

同时, 所有的Value对象都可以调用Interface方法, 来将其转回Interface{}对象, 然后再通过类型断言进行转换.

基础类型

基础类型的取值, GO提供了对应的方法, 使用起来也很简单.

// 基础类型取值
a := int64(3)
valueOf := reflect.ValueOf(&a)
// 取基础类型.
// 注意, 若不是相关类型, 会报错. 可查看源码
// 所有的整形, 都返回 int64, 若需要 int32, 可拿到返回值后强转
fmt.Println(valueOf.Int())
//fmt.Println(valueOf.Float())
//fmt.Println(valueOf.Uint())
// ... 等等

结构体

如果是自定义的结构体怎么取值呢? 这, 一直找到基础类型. 因为所有的自定义结构体都是有基础类型组成的嘛.

u := struct {
Name string
Age  int
}{"xiao ming", 20}
valueOf := reflect.ValueOf(u)
fmt.Println(valueOf.Field(0).String())

数组

如果是数组呢? 也很简单

l := []int{1, 2, 3}

valueOf := reflect.ValueOf(l)
// 修改指定索引的值
fmt.Println(valueOf.Elem().Index(0))
// 获取数组长度
fmt.Println(valueOf.Elem().Len())

map

通过反射获取Map的值时, 取到的是Value对象, 同时要使用Value对象进行取值. 毕竟Mapkeyvalue类型都是不固定的嘛.

m := map[string]string{
"a": "1",
}
valueOf := reflect.ValueOf(m)
// 获取指定索引的值
fmt.Println(valueOf.MapIndex(reflect.ValueOf("a")))
// 若指定索引的值不存在, 会返回一个 kind 为 Invalid 的 Value 对象
fmt.Println(valueOf.MapIndex(reflect.ValueOf("c")))
//  取 map 大小
fmt.Println(valueOf.Len())
// 获取 map 的所有 key, 返回 Value 对象列表
fmt.Println(valueOf.MapKeys())
// 遍历 map 用的迭代器
mapIter := valueOf.MapRange()
mapIter.Next() // 将迭代指针直线下一个, 返回是否还有数据
fmt.Println(mapIter.Value())
fmt.Println(mapIter.Key())

属性赋值

基础类型的赋值, reflect.Value对象提供了相关方法, 都以 Set 开头.

这里注意, 只有指针类型的的变量才能被赋值. 其实很好理解, 值类型在方法调用时是通过复制传值的. 只有传递指针才能够找到原始值的内存地址进行修改.

故, 我们在赋值之前, 要调用Kind方法对其类型进行判断, 若不是通过指针创建的Value对象, 一定不能赋值.

以下所有的赋值操作, 都可以与取值操作联动进行.

基础类型

a := int64(3)
valueOf := reflect.ValueOf(a)
// 此方法用于判断 Value 对象是否可以赋值
valueOf.CanSet()
// 因为是指针, 所以这里需要进行取值操作
valueOf.Elem().SetInt(20)
fmt.Println(a)

结构体

结构体的赋值与上面的获取属性值相同, 使用指针获取Value对象, 然后对其基础类型赋值.

需要注意的一点, 结构体只有公共字段才能够通过反射进行赋值, 若赋值给一个私有字段, 会抛出异常.

u := struct {
  Name string
  Age  int
}{"xiao ming", 20}

valueOf := reflect.ValueOf(&u)
valueOf.Elem().Field(0).SetString("xiao hei")
fmt.Println(u)

数组

其基础的Set方法确实提供了很多, 但是我查了一圈, 对于数组类型怎么赋值呢? 于是我看到了这个方法:

func (v Value) Set(x Value)

这个Set方法, 接收的参数是Value对象? 那不就成了么. 注意, Set是直接进行替换, 而不是追加.

l := []int{1, 2, 3}

valueOf := reflect.ValueOf(&l)
// 创建一个数组用于后面进行赋值
// 注意, 数组类型要相同
setValueOf := reflect.ValueOf([]int{4, 5})
valueOf.Elem().Set(setValueOf)
fmt.Println(l)

// 修改指定索引的值
// 通过指针, 将指定索引的值取出来后, 进行赋值
valueOf.Elem().Index(0).SetInt(9)
fmt.Println(l)

map

m := map[string]string{
  "a": "1",
}
valueOf := reflect.ValueOf(&m)
// 给指定的 key 设置
valueOf.Elem().SetMapIndex(reflect.ValueOf("b"), reflect.ValueOf("2"))
fmt.Println(m)

创建空值 Value

除了上面的赋值操作, 还有一种不需要判断对象类型的方式, 通过方法New, 可以创建一个相同类型的空值Value对象, 返回的是一个指针的Value类型.

此操作的好处是, 在使用过程中, 完全不需要判断对象类型.

a := int64(3)
// 创建一个相同类型内容. 返回的是一个指针
fmt.Println(reflect.New(reflect.TypeOf(a)).Elem())

变量方法的操作

普通方法

普通方法指未依附与结构体的方法.

func add(a, b int) int {
    return a + b
}

func main() {
    valueOf := reflect.ValueOf(add)
    // 构造函数参数
    paramList := []reflect.Value{reflect.ValueOf(2), reflect.ValueOf(3)}
    // 调用函数. 返回一个 Value 数组
    retList := valueOf.Call(paramList)
    // 获取返回值
    fmt.Println(retList[0].Int())
}

结构体方法

获取方法信息

这里需要注意, 结构体指针和对象所拥有的方法数量是不同的, 具体可看: https://hujingnb.com/archives/348

type User struct {
    Name string
}

func (u User) GetName() string {
    return u.Name
}

func (u *User) SetName(name string) {
    u.Name = name
}

func main() {

    u := User{}
    typeOf := reflect.TypeOf(&u)
    // 获取结构体中方法数量. 私有方法是拿不到的
    fmt.Println(typeOf.NumMethod())
    // 获取第0个方法, 返回 Method 对象. 下面介绍
    fmt.Println(typeOf.Method(0))
    // 根据方法名获取, 返回 Method 对象
    fmt.Println(typeOf.MethodByName("GetName"))

    /* Method 对象 */
    setNameFunc, _ := typeOf.MethodByName("GetName")
    // 方法名
    fmt.Println(setNameFunc.Name)
    // 方法签名
    fmt.Println(setNameFunc.Type)
    fmt.Println(setNameFunc.Index)
    // 字段的可访问的包名. 公共方法为空
    fmt.Println(setNameFunc.PkgPath)
}

方法调用

type User struct {
    Name string
}

func (u User) GetName() string {
    return u.Name
}

func (u *User) SetName(name string) {
    u.Name = name
}

func main() {

    u := User{}
    valueOf := reflect.ValueOf(&u)
    // 获取结构体中方法数量. 私有方法是拿不到的
    fmt.Println(valueOf.NumMethod())
    // 获取第0个方法, 返回 Method 对象. 下面介绍
    fmt.Println(valueOf.Method(0))
    // 根据方法名获取, 返回 Method 对象
    fmt.Println(valueOf.MethodByName("GetName"))

    /* Method 对象 */
    setNameFunc := valueOf.MethodByName("SetName")
    // 调用方法
    params := []reflect.Value{reflect.ValueOf("xiao ming")}
    setNameFunc.Call(params)
    // 此时对象的值已经改变
    fmt.Println(u)
    // 接收方法返回值
    getNameFunc := valueOf.MethodByName("GetName")
    fmt.Println(getNameFunc.Call([]reflect.Value{}))
}

------------

原文地址 https://hujingnb.com/archives/676

转载请保留原文连接: Golang 反射操作整理 | 烟草的香味

guest
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请发表评论。x