golang中的反射
什么是反射
反射是 go 语言提供的一种在运行时动态获取变量类型和值的手段。
有时候我们可能需要编写一个函数,来处理当时尚不清楚类型的参数,可能是该类型当时尚不存在,也可能是其没有明确的表达式。
比如说,我要编写一个函数,接收一个 any 类型的参数,打印出该参数的实际类型以及其方法列表。该需求通过正常方案是没办法实现的,必须使用反射,来得到其类型和其方法列表。
反射的使用
所有反射的功能,均由 reflect
包提供。该包中包含了反射功能最核心的两个类型,一是 reflect.Type
,该类型用来描述一个变量的具体类型;二是 reflect.Value
,该类型用来描述一个变量的具体的值。
reflect.TypeOf
函数 reflect.TypeOf()
接收任意的类型,并以 reflect.Type
返回其具体类型。
1 | t := reflect.TypeOf(3) // a reflect.Type |
这里额外提一嘴,将一个具体类型的值,作为接口类型传递时,在内部其实做了一个隐式的接口转换,它会创建一个包含两个信息的接口值,一个表示其具体类型(此处是 int),另一个表示其具体的值(此处是3)。
所以当你通过 TypeOf 传递一个接口类型的具体实现时,得到的将会是其具体类型,而非接口类型。说起来比较晦涩,可以通过以下例子理解一下:
1 | var w io.Writer = os.Stdout |
reflect.ValueOf
函数 reflect.ValueOf()
同样接受一个 any 类型的参数,并返回一个装载其值的 reflect.Value
类型。该类型可以用来装载任意类型的值。
1 | v := reflect.ValueOf(3) // a reflect.Value |
reflect.ValueOf 的逆操作是 reflect.Value.Interface()
方法。它返回一个 any 类型,装载着与 reflect.Value
相同的具体值。
1 | v := reflect.ValueOf(3) // a reflect.Value |
any 类型和 reflect.Value 类型都可以用来表示任何值,但不一样的是 any 类型隐藏了其内部的所有实现,我们只有知道其具体类型并将其转换为具体类型,才能访问其内部数据乃至其方法。而 Value 类型则提供了很多方法来访问其内部数据,无论它的具体类型是什么。
Kind
在 go 中,由于数组、切片乃至 map 的存在,其数据类型可以说是无穷无尽的。因为 [1]int
和 [2]int
表示的是两种类型,同样的 map[int]string
和 map[int64]string
表示的也是不同的类型。这时候我们就需要知道这种类型的种类了,在 go 中,我们将称其为 Kind
。
reflect 包中定义了一个 Kind 类型,并罗列了所有种类对应的 Kind 值,如下所示:
1 | type Kind uint |
Kind | Value | Kind | Value | Kind | Value |
---|---|---|---|---|---|
Invalid | 0 | Bool | 1 | Int | 2 |
Int8 | 3 | Int16 | 4 | Int32 | 5 |
Int64 | 6 | Uint | 7 | Uint8 | 8 |
Uint16 | 9 | Uint32 | 10 | Uint64 | 11 |
Uintptr | 12 | Float32 | 13 | Float64 | 14 |
Complex64 | 15 | Complex128 | 16 | Array | 17 |
Chan | 18 | Func | 19 | Interface | 20 |
Map | 21 | Pointer | 22 | Slice | 23 |
String | 24 | Struct | 25 | UnsafePointer | 26 |
可以通过 reflect.Type.Kind()
来获取一个类型的种类。
有了 Kind 的加持,我们基本上就可以实现解析所有类型的数据了。
1 | func parseType(v any) { |
运行结果:
1 | [5]int : [ |
操控 reflect.Value
虽然 reflect.Value 类型带有很多方法,但是只有少数的方法能对任意值都安全调用。例如,Index 方法只能对 Slice、数组或字符串类型的值调用,如果对其它类型调用则会导致 panic 异常。
以下总结一下各种类类型能够使用的方法:
Kind | Methods |
---|---|
Array | Len ,Cap ,Index ,Slice ,Slice3 |
Slice | Len ,Cap ,Index ,Slice ,Slice3 ,Append ,SetLen ,SetCap |
Chan | Cap ,Len ,Close ,Recv ,RecvDir ,Send ,SendDir ,TryRecv ,TrySend |
Func | Call ,CallSlice ,IsNil |
Interface | Elem ,IsNil |
Map | Len ,IsNil ,Keys ,MapIndex ,MapKeys ,MapRange ,MapIter ,SetMapIndex ,Delete |
Ptr | Elem ,IsNil ,Pointer ,Set |
Struct | Field ,FieldByIndex ,FieldByName ,FieldByNameFunc ,NumField |
UnsafePointer | Pointer |
String | Len ,String |
Bool | Bool |
Int、Int8、Int16、Int64 | Int |
Int32 | Int ,Rune |
Uint8 | Uint ,Bytes |
Uint、Uint16、Uint32、Uint64、Uintptr | Uint |
Float32、Float64 | Float |
Complex64、Complex128 | Complex |
在反射的使用场景中,很大部分是需要对参数的值进行修改的,譬如说 encoding/json
包中的 Unmarshal
函数,会将 json 数据解析到一个结构体参数中,此处传递的参数必须要是一个结构体指针。
此时,就需要涉及到 reflect.Value 的另外一个属性了,即可修改性。要对一个变量进行修改,首先要满足两个条件:
- 该变量是可到达的
- 该变量是可修改的(非导出字段是禁止修改的)
回想一下,Go语言中类似x、x.f[1]和*p形式的表达式都可以表示变量,但是其它如x + 1和f(2)则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。
对于reflect.Values也有类似的区别。有一些reflect.Values是可取地址的;其它一些则不可以。考虑以下的声明语句:
1
2
3
4
5 x := 2 // value type variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes (x)其中a对应的变量不可取地址。因为a中的值仅仅是整数2的拷贝副本。b中的值也同样不可取地址。c中的值还是不可取地址,它只是一个指针&x的拷贝。实际上,所有通过reflect.ValueOf(x)返回的reflect.Value都是不可取地址的。但是对于d,它是c的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的Value。
reflect.Value 提供了两个方法来判断其是否满足上面的两个条件,一个是 value.CanAddr()
,用来判断该 value 是否可以被取地址;另一个是 value.CanSet()
,用来判断该 value 的值是否能被修改。
1 | s := struct { |
1 | false false |
在确定 reflect.Value 的值是可修改的后,就可以调用 reflect.Value 的一系列 Set
方法来修改变量的值了。
1 | value.Set() |
使用这一系列的 Set 方法时需要注意,以下两种情况会导致程序 Panic:
- value 为不可到达或不可修改的值
- 设置的类型与 value 的实际类型不符
若是不想使用 Set 系列方法,也能通过取地址、断言并赋值等手段来修改值,不过这种方式比较繁琐,不是很推荐。
1 | v := 10 |
结构体中的用法
在 go 中,结构体字段的标签,也是结构体定义的一个部分。一般情况下,我们只有通过反射包,才能利用定义的这个标签。
此处我们用一段程序代码来演示相关用法。
当使用 go 语言开发 HTTP 接口时,经常需要获取客户端传递过来的接口参数,并将其映射到结构体中,此处的映射就是使用反射实现的。
1 | func parseBody(req *http.Request, body any) error { |
可以阅读以上代码和注释,了解一下结构体和反射结合使用的姿势。此处简单总结一下常用的几个方法:
NumField() int
:Type 和 Value 都能调用该方法,返回其字段数量func (Type)Field(i int) StructField
:返回其字段信息func (v Value) Field(i int) Value
:返回字段的值func (tag StructTag) Get(key string) string
:返回指定标签的值
结语
通过反射可能方便我们编写出一些正常情况下无法实现的功能,但是使用反射是很危险的事情,在使用反射前需要反复确定反射是否是必要的。因为使用反射,你的代码可能会出现以下问题:
- 反射代码的错误在编译时无法检测,可能会在程序运行后甚至运行很长一段时间后才暴露出来
- 反射代码的可读性很差,维护的成本很高
- 反射代码的执行效率通常比正常代码低一到两个数量级
因此,一般建议反射只在必要的时候使用,且需要编写齐全的注释以方便后面维护该代码的程序员能读懂它。