写在开头
非原创,知识搬运工
demo代码地址
往期回顾
带着问题去阅读
- 反射是把双刃剑,列举缺点
- 反射和接口的关系
- 反射的三大法则
- interface、type和value如何互相转化
- 列举反射应用场景
- 列举几个反射使用不当引发panic的场景
1.什么是反射
《GO语言圣经》对反射的描述是
Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。
也就是说它能让程序在运行时,能探知到一个对象的类型信息和内存结构,因此我们能联想到几个反射的应用场景:
1.当函数传入的参数为interface{}时(即没约定好),我们可以使用断言判断,也可以使用反射确定
2.根据某些条件(比如不确定的参数)决定调用哪个函数,可以根据反射来动态确定调用哪一个函数
总结来说就是在运行期间将不确定的因素转为确定(感觉很像断言的机制)
1.1反射的缺点
1.代码难阅读,不好维护
2.反射代码中的错误不容易被编译器发现,从而引发panic
3.反射性能差,运行速度慢
2.使用反射
2.1接口
反射与接口的结构,特别是空接口的结构体有很大的关联,我们复习下第4节接口的部分知识
接口分为两种类型:
- 带方法的接口,底层是iface
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
其中_type表示的是实体类型,即具体类型,inter表示的是接口类型--由具体类型实现的接口类型,两者分别包含了接口和实现类的各种信息

- 空接口interface{} ,底层是eface
type eface struct {
_type *_type
data unsafe.Pointer
}
只有一个具体类型_type
当iface的动态值data和动态值_type都为nil时,接口==nil,iface的inter也叫做静态类型
var w io.Writer
io.Writer是一个接口,是w的静态类型
另外当函数的参数类型是interface{}时,会做一个隐式类型转换
2.2 反射与接口的关系
reflect包中定义了一个接口和一个结构体,即Type和Value
- Type提供类型相关信息,与接口结构的_type关系紧密
- Value则结合_type和data两者,因此我们可以获取甚至改变类型的值
2.2.1 Type
TypeOf是获取Type的方法
func TypeOf(i any) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
//实际上就是类型转换
func toType(t *rtype) Type {
if t == nil {
return nil
}
return t
}
注意1.18 参数类型是interface{},1.20是any实际是语法糖,emptyinterface和eface是一回事,字段相同,eface在reflect包中,emptyinterface在runtime包中
type emptyInterface struct{
typ *rtype
word unsafe.Pointer
}
toType返回的 *rtype是一个实现了Type的接口,对Type调用方法实际上是对_type的操作
2.2.2Value
先看一下value的结构体
type Value struct {
typ *rtype
ptr unsafe.Pointer
flag
}
type flag uintptr
*rtype现在知道了吧,是一个Type接口的实现类即动态类型,ptr存放动态值
ValueOf获取Value
func ValueOf(i any) Value {
if i == nil {
return Value{}
}
//内存操作,让值在堆中
escapes(i)
return unpackEface(i)
}
func unpackEface(i any) Value {
e := (*emptyInterface)(unsafe.Pointer(&i))
t := e.typ
if t == nil {
return Value{}
}
f := flag(t.Kind())
if ifaceIndir(t) {
f |= flagIndir
}
return Value{t, e.word, f}
}
我们主要关心unpack函数:
1.转为eface
2.获取动态类型和动态值
3.组装数据
那么既然value有typ字段,说明value可以转为Type接口(具体实现方法就是Value.Type)
小结一下关系

这里我们对两者的其他方法不再过多分析
2.3三大法则
关于反射GO官方博客总结了三大法则
1.Reflection goes from interface value to reflection object.
2.Reflection goes from reflection object to interface value.
3.To modify a reflection object, the value must be settable.
1.反射是一种检测存储在interface{}中的类型和值机制。(通过TypeOf和ValueOf得到)
2.第二条与第一条相反,反射对象(ValueOf返回值)通过interface()函数反向转变成interface变量,也就是说接口和反射类型是可以互相转换
3.要修改反射对象,其值必须可以修改。如果反射变量可设置(因为反射变量存储了原变量本身),对反射变量的操作就会反应到原变量上;反之,如果不可设置,即反射变量不能代表原变量,那么操作反射变量不会对原变量产生影响,这种情况在语言层面是不能被允许的
demo
func TestChange(t *testing.T) {
var x int64 = 1
v := reflect.ValueOf(x)
v.SetInt(2)
t.Log(x)
}
这段代码编译器能通过,但实际上会引发panic(这也验证了上文反射代码错误能逃过编译器检查的观点)
panic: reflect: reflect.Value.SetInt using unaddressable value [recovered]
panic: reflect: reflect.Value.SetInt using unaddressable value
提示了我们使用了一个不可寻址的值,我们在函数那一章提到了GO函数只有值传递,因此ValueOf传入的是一个变量的拷贝,ValueOf操作的是函数栈帧中的参数段,这里参数段与局部变量段没有联系,因此参数段不能代表局部变量段的x,不符合第三法则。
怎么解决?之前让函数参数段和局部变量段有关联,我们不就用了指针吗
v := reflect.ValueOf(&x)
需要注意的是此时传入的是一个地址,我们用Elem函数获取真正的x
var x int64 = 1
v := reflect.ValueOf(&x)
// v := reflect.ValueOf(x)
v.Elem().SetInt(2)
t.Log(x)
//2
所以在使用反射时尽量使用指针(如果要操作变量)
2.4 结构体成员
正常情况下,结构体中可以被修改的成员只有导出成员,即首字母大写(表示结构体外可访问),对于未导出,只能读,不能修改,否则引发panic
3.reflect实际应用场景
IDE代码自动补全,对象序列化(json库),fmt相关函数,ORM等都涉及到反射reflect
网友评论