Golang 反射使用的一些技巧

Some tips for using reflection in Golang

foreversmart write on 2023-03-23

What

在 Golang 中 reflect包实现了运行时反射,允许程序操作任意类型的对象。反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
结合 Golang 中的 interface{} ,可以实现一些类似泛型的效果,比如下面这个函数
Go
Copy
func Minimum(first interface{}, rest ...interface{}) interface{} { //... }
这样就使得静态的go有了很多动态的特性,常见的用法是用静态类型interface{}保存一个值,通过调用TypeOf获取其动态类型信息,该函数返回一个Type类型值。调用ValueOf函数返回一个Value类型值,该值代表运行时的数据。Zero接受一个Type类型参数并返回一个代表该类型零值的Value类型值。

类型(Type)与种类(Kind)

在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。
种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:
Go
Copy
type Kind uint const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Ptr Slice String Struct UnsafePointer )
type 是更具体的类型,很多时候变量的类型的名称可能和 kind 相等. 在 reflect 包中 type 是一个 interface,由具体的类型实现了这些方法。
Go
Copy
type A struct { } var a A var b int64 tA := reflect.TypeOf(a) fmt.Println(tA.Name(), tA.Kind()) // A struct tB := reflect.TypeOf(b) fmt.Println(tB.Name(), tB.Kind()) // int64 int64

Value 值

反射中的 Value 是 Golang 变量值在反射中的表示类型,Vaule 是一个 struct 类型
通过 Value 上提供的方法把 Value 转换成正常的变量值,也可以对底层的值类型进行操作和修改。不是所有的 Value 上的方法都可以正常的调用,需要根据 value 的 kind 来决定哪些方法可以被调用,调用错方法会抛出运行时的 panic

反射的使用

三个规则:

Reflection goes from interface value to reflection object
反射是通过接口获取的反射对象,反射里面基础两个反射对象前面讲到的 Type 和 Value 可以通过下面这两个函数获取
Go
Copy
reflect.TypeOf() // reflect.Type reflect.ValueOf() // reflect.Value
这两个函数的参数类型就是 interface{}
Reflection goes from reflection object to interface value
就是说可以通过反射对象 Value 获取 interface 值, 通过 interface 转换成其它的变量类型
Go
Copy
func (v Value) Interface() interface{}
也可以通过一些直接的辅助方法获取 value 的底层类型
Go
Copy
func (v Value) Float() float64 func (v Value) Int() int64 func (v Value) Pointer() uintptr func (v Value) String() string
To modify a reflection object, the value must be settable
通过反射修改底层对象的值,必须要求这个底层对象的值是可以设置的
Shell
Copy
var x float64 = 3.14 v := reflect.ValueOf(x) v.SetFloat(7.2) // Error: will panic.
因为调用 ValueOf 方法的时候发生了对 x 的拷贝, 所以对 x 的拷贝不能修改真正存储值的 data 所以这个 x 不具备 settability
我们修改我们的代码使用 x 的指针 &x reflect.ValueOf(&x) 得到的 value 是 x 的地址同样是不具备 settability 的, 但是我们可以获取 x 指向的 value reflect.ValueOf(&x).Elem() 就可以修改到 x 的值了。注意这里 x 的类型不管是不是指针类型,都会发生 copy 从而导致直接进行形参是不具备 settability 的
Shell
Copy
v := reflect.ValueOf(&x).Elem() v.SetFloat(7.2) fmt.Println(x, "x") // 7.2 x
我们可以通过 v.CanSet() 来判断一个 value 是不是可以被设置的

CanSet 和 CanAddr 方法区别

CanAddr 官方的定义
Plain Text
Copy
CanAddr reports whether the value's address can be obtained with Addr. Such values are called addressable. A value is addressable if it is an element of a slice, an element of an addressable array, a field of an addressable struct, or the result of dereferencing a pointer. If CanAddr returns false, calling Addr will panic.
可寻址是可以通过反射获得其地址的能力。可寻址与指针紧密相关。所有通过reflect.ValueOf()得到的reflect.Value都不可寻址。因为它们只保存了自身的值,对自身的地址一无所知。例如指针p *int保存了另一个int数据在内存中的地址,但是它自身的地址无法通过自身获取到,因为在将它传给reflect.ValueOf()时,其自身地址信息就丢失了
对于非指针的类型我们需要传递一个指针通过 v.Elem() 或者 reflect.Indirect(v) 的方式获得引用的对象的地址,map 和 pointer 不需要传递指针,因为他们本来就是指针,而 slice 是一个 SliceHeader 的结构,需要传递指针
CanSet 比 CanAddr 范围更小,在处理 struct 里面的私有字段时,这个字段是可以寻址但是不能被修改的
具体官方的实现源码如下:
Plain Text
Copy
func (v Value) CanAddr() bool { return v.flag&flagAddr != 0 } func (v Value) CanSet() bool { return v.flag&(flagAddr|flagRO) == flagAddr }
CanSet 多判断了一个 RO 标志位判断是否是只读的

Elem() 和 Indirect() 的区别

当 value 是指针时他们的作用是相同的,都是返回指针指向的对象
当 value 的类型是 interface 类型是如果底层类型是指针类型 Elem() 会指向底层的指针对象, 而 Indirect() 会返回这个 value 本身
如果是其它类型,或者 value 是 interface 类型但是底层类型不是指针类型 Elem() 会报错,而 Indirect 返回 value 本身
Go
Copy
c := 15 var a *int a = &c var b interface{} b = a v := reflect.ValueOf(&b) vv := v.Elem() vvv := vv.Elem() vvvv := vvv.Elem() v1 := reflect.Indirect(v) v2 := reflect.Indirect(v1) v3 := reflect.Indirect(v2) fmt.Println(v1.Type(), v1.Interface(), v1.CanAddr()) fmt.Println(v2.Type(), v2.Interface(), v2.CanAddr()) fmt.Println(v3.Type(), v3.Interface(), v3.CanAddr()) fmt.Println(vv.Type(), vv.Interface(), vv.CanAddr()) fmt.Println(vvv.Type(), vvv.Interface(), vvv.CanAddr()) fmt.Println(vvvv.Type(), vvvv.Interface(), vvvv.CanAddr()) /* 输出 interface {} 0xc000012028 true interface {} 0xc000012028 true interface {} 0xc000012028 true interface {} 0xc000012028 true *int 0xc000012028 false int 15 true */

场景和技巧

初始化一个值

新建一个和变量 v 底层类型相同的 value 对象
Go
Copy
vv := reflect.ValueOf(v) vt := vv.Type().Elem() nv := reflect.New(vt)

通过反射修改变量的值

Go
Copy
a := 15 v := reflect.Indirect(reflect.ValueOf(&a)) v.SetInt(23) fmt.Println(v.CanAddr(), a, v.Int()) // true 23 23

类型获取

可以通过 reflect.TypeOf() 函数获取 一个变量的类型,但是如果 value 是 interface{} 类型时,会直接获取到底层的对象的类型而不是 interface 类型,想获取 interface 的类型需要传递一个引用进去,然后获取这个引用的类型是一个 *Interface 类型,通过 Elem() 再获取到 interface{} 的类型
Go
Copy
a := 10 h := &A{} var i interface{} i = h var c HelloI c = h fmt.Println(reflect.TypeOf(a), reflect.TypeOf(&a), reflect.TypeOf(h), reflect.TypeOf(i), reflect.TypeOf(&i), reflect.TypeOf(c), reflect.TypeOf(&c)) // int *int *main.A *main.A *interface {} *main.A *main.HelloI
获取接口的反射类型
Go
Copy
t := reflect.TypeOf((*HelloI)(nil))
直接通过 HelloI 接口是无法获取的

Struct 使用

当一个 v reflect.Value() 的类型为 struct 的时候:
成员变量的值:
v.Field(i) v.FieldByName(name)
成员变量的类型:
v.Field(i).Type() 获取
但是结构体字段信息在 v 的类型里
v.Type().Field(i) 这样可以获取里面字段的类型以及 Tag 信息
v.Type().Field(i).Tag
v.Type().Field(i).Type

指针

v.Elem() 获取指针指向的目标值
v.Elem().Type() 获取执向的类型
但是对于空指针会报错
Shell
Copy
panic: reflect: call of reflect.Value.Type on zero Value
这个时候我们需要使用
v.Type().Elem() 来获取指针指向的对象的类型

Map 使用

可以通过 v.Type().Key()v.Type().Elem() 分别获取 map 的 key value 的类型 特别是在 map 没有初始化的时候
已经初始化的 map 可以通过
v.MapKeys() v.MapIndex() v.MapRange() 获取 map 的 key value 以及遍历
初始化一个 map
reflect.MakeMap(typ) 我们可以通过一个 kind 是 map 的 type 初始化一个 map
设置 Map 的值
SetMapIndex(key, value) 来设置 Map 的 key value,如果 value 的值为 zero 相当于 删除 map key,value 的值

数组使用

数组初始化:
Shell
Copy
newArray := reflect.MakeSlice() v.Set(newArray)
Slice 添加元素
relect.Append()
元素类型
v.Type().Elem()
设置数组值
v.Index(i).Set(v1)

Interface

在通过 reflect.ValueOf(i) 进行取值的时候, 根据文档:
Plain Text
Copy
// ValueOf returns a new Value initialized to the concrete value // stored in the interface i. ValueOf(nil) returns the zero Value.
所以对于一个 var i interface{} i = 1 的接口取值的时候
reflect.ValueOf(i).Kind() == int
只有当对 interface{} 的指针 或者 struct 的 field 的时候可以获取 kind == interface
Shell
Copy
reflect.ValueOf(&i).Elem.Kind() == interface reflect.ValueOf(struct{ i interface{} }{0}).Field(0).Kind == interface
而对于没有初始化的值
Shell
Copy
var a interface{} v := reflect.ValueOf(a).Kind() // invalid
获取指向循环问题
Shell
Copy
var a interface{} a = &a reflect.ValueOf(a) // ptr reflect.ValueOf(a).Elem() // interface reflect.ValueOf(a).Elem().Elem() // ptr reflect.ValueOf(a).Elem().Elem().Elem() // interface
设置 interface 的值
如果一个 v 的 kind == interface 你不能直接将 v 设置为 别的基础类型
比如: v.SetInt() v.SetFloat() 都会报错
Shell
Copy
panic: reflect: call of reflect.Value.SetFloat on interface Value [recovered] panic: reflect: call of reflect.Value.SetFloat on interface Value
可以通过 v.Set(reflect.ValueOf(v1)) 的方式进行设置
对于没有初始化的 interface{} value 会报错 且前面说到他的类型是 invalid 的:
Shell
Copy
panic: reflect: call of reflect.Value.Set on zero Value [recovered] panic: reflect: call of reflect.Value.Set on zero Value
对于没有初始化的 interface{} 的 value 调用 type 方法也会报错
Shell
Copy
panic: reflect: call of reflect.Value.Type on zero Value [recovered] panic: reflect: call of reflect.Value.Type on zero Value
在 struct 里面的 interface 调用 type 和 value 和设置值是可以的,但是 没有初始化的不行
Shell
Copy
type A struct { F interface{} } { var i interface var a A vb := reflect.ValueOf(a).Field(0).Type() // interface{} va := reflect.ValueOf(i).Type() // panic: reflect: call of reflect.Value.Type on zero Value [recovered] // panic: reflect: call of reflect.Value.Type on zero Value // 数组 和 map 里面的 interface 也同样会报 }
根据反射的第三个规则,我们可以对其取指针进行值修改
Shell
Copy
reflect.ValueOf(&i).Elem().Set(reflect.ValueOf(1)) fmt.Println(i) // 1

修改私有属性

Golang 中反射是不能对私有成员变量进行赋值的,调用 CanSet() 得到的值是 false
可以通过直接设置整个原对象的方式修改成员变量
另外还有一种方式就是通过 unsafe 来修改成员变量对于结构体中的成员会被分配在一块连续的内存上,结构体的地址代表了第一个成员的地址。然后通过计算偏移量的方式来计算其他成员的地址。
Go
Copy
type Student struct { ID string Name string } func SetName(s * Student) { ID := (*string)(unsafe.Pointer(&s)) *ID = "id1" Name := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.Name))) *Name = "first_name" }

反射判断 v 是否实现某个接口

Go
Copy
v.Implements(reflect.TypeOf((*HelloI)(nil)).Elem())

参考:

「真诚赞赏,手留余香」

Foreversmart

真诚赞赏,手留余香

使用微信扫描二维码完成支付