4.3.3 更新变量
当我们想要更新一个reflect.Value
,就需要调用reflect.Value.Set
方法更新反射对象,该方法会调用reflect.flag.mustBeAssignable
和reflect.flag.mustBeExported
分别检查当前反射对象是否可以被设置值以及字段是否对外公开:
func (v Value) Set(x Value) {
v.mustBeAssignable()
x.mustBeExported()
var target unsafe.Pointer
if v.kind() == Interface {
target = v.ptr
}
// 将x的值转换到与v相同的类型
x = x.assignTo("reflect.Set", v.typ, target)
// 将x的值移动到v
typedmemmove(v.typ, v.ptr, x.ptr)
}
reflect.Value.Set
方法会调用reflect.Value.assignTo
并返回一个新的反射对象,这个返回的反射对象指针就会直接覆盖原始的反射变量。
func (v Value) assignTo(context string, dst *rtype, target unsafe.Pointer) Value {
// ...
switch {
// 如果v的类型可以直接赋值给dst
case directlyAssignable(dst, v.typ):
// ...
// 以目标类型dst为类型,Value的当前ptr为值,返回新Value实例
return Value{dst, v.ptr, fl}
// 如果v实现了目标接口dst,此时我们需要将v转换为dst表示的接口类型
case implements(dst, v.typ):
// 如果v是一个接口,且值为nil
if v.Kind() == Interface && v.IsNil() {
// 以目标接口dst为类型,nil为值,返回新Value实例
return Value{dst, nil, flag(Interface)}
}
// 将v转换为空接口(interface{})类型
x := valueInterface(v, false)
// 如果要转换到的目标接口是空接口(没有任何方法)
if dst.NumMethod() == 0 {
// target即将用于Value实例的初始化,作为指向实际数据的指针
// 此处将target指向v转换后的空接口类型
*(*interface{})(target) = x
} else {
// 将空接口x类型转换成接口dst
ifaceE2I(dst, x, target)
}
// 以target为值,dst为类型,创建一个Value实例
return Value{dst, target, flagIndir | flag(Interface)}
}
panic(context + ": value of type " + v.typ.String() + " is not assignable to type" + dst.String())
}
reflect.Value.assignTo
会根据当前和被设置的反射对象类型创建一个新的Value
结构体:
1.如果两个反射对象的类型是可以被直接替换,就会直接将目标反射对象返回;
2.如果当前反射对象是接口并且目标对象实现了接口,就会将目标对象简单包装成接口值;
在变量更新过程中,reflect.Value.assignTo
返回的reflect.Value
中的指针会覆盖当前反射对象中的指针实现变量的更新。
4.3.4 实现协议
reflect
包还为我们提供了reflect.rtypes.Implements
方法可以用于判断某些类型是否遵循特定的接口。在Go语言中获取结构体的反射类型reflect.Type
还是比较容易的,但是想要获得接口的类型就需要通过以下方式:
// 此处的<interface>应该被替换为具体的接口类型
reflect.TypeOf((*<interface>)(nil)).Elem()
// 如果是获取非接口类型的reflect.Type,直接获取即可
reflect.TypeOf(1)
// 为什么会有区别?首先reflect.TypeOf(x)获取的是变量x当前持有具体类型的reflect.Type
// 而当x是(*<interface>)(nil)时,(*<interface>)(nil)才是接口本身的类型信息
我们通过一个例子来介绍如何判断一个类型是否实现了某个接口。假设我们需要判断如下代码中的CustomError
是否实现了Go语言标准库中的error
接口:
type CustomError struct{}
func (*CustomError) Error() string {
return ""
}
func main() {
typeOfError := reflect.TypeOf((*error)(nil)).Elem()
customErrorPtr := reflect.TypeOf(&CustomError{})
customError := reflect.TypeOf(CustomError{})
fmt.Println(customErrorPtr.Implements(typeOfError)) // #=> true
fmt.Println(customError.Implements(typeOfError)) // #=> false
}
上述代码的运行结果正如我们在接口一节中介绍的:
1.CustomError
类型没有实现error
接口;
2.*CustomError
指针类型实现了error
接口;
抛开上述的执行结果不谈,我们来分析一下reflect.rtypes.Implements
方法的工作原理:
func (t *rtype) Implements(u Type) bool {
if u == nil {
panic("reflect: nil type passed to Type.Implements")
}
if u.Kind() != Interface {
panic("reflect: non-interface type passed to Type.Implements")
}
// rtype类型实现了Type接口,此处将接口u类型断言为rtype类型
return implements(u.(*rtype), t)
}
reflect.rtypes.Implements
方法会检查传入的类型是不是接口,如果不是接口或者是空值就会直接panic中止当前程序。在参数没有问题的情况下,上述方法会调用私有函数reflect.implements
判断类型之间是否有实现关系:
// rtype是类型信息,它也是接口类型的底层结构iface里的typ字段
func implements(T, V *rtype) bool {
// 将T转换为interfaceType类型,这里使用unsafe.Pointer是为了绕过go的类型安全检查
t := (*interfaceType)(unsafe.Pointer(T))
// 如果接口没有方法
if len(t.methods) == 0 {
// 返回true,表示任何类型都实现了该接口
return true
}
// ...
// 获取V的非常见方法集,包括嵌入的类型或通过反射动态添加的方法
v := V.uncommon()
i := 0
// 获取方法集的切片
vmethods := v.methods()
// 遍历V的所有方法
for j := 0; j < int(v.mcount); j++ {
// 获取t的当前偏移的方法
tm := &t.methods[i]
// 获取t当前偏移方法的信息
tmName := t.nameOff(tm.name)
// 获取v的当前偏移的方法
vm := vmethods[j]
// 获取v当前偏移方法的信息
vmName := V.nameOff(vm.name)
// 如果方法名和方法类型都相同
if vmName.name() == tmName.name() && V.typeOff(vm.mtyp) == t.typeOff(tm.typ) {
// 递增t的方法偏移,如果遍历完了t的所有方法
if i++; i >= len(t.methods) {
// 所有方法均已实现,返回true
return true
}
}
}
return false
}
如果接口中不包含任何方法,就意味着这是一个空的接口,任意类型都自动实现该接口,这时就会直接返回true
。
在其他情况下,由于方法都是按照字母序存储的,reflect.implements
会维护两个用于分别遍历接口和类型的方法的索引i
和j
,来判断是否实现了接口,因为最多只会进行n+m
次数的比较,所以整个过程的时间复杂度是O(n+m)
(不太清楚这里的m和n分别表示什么,应该分别是接口和类型的方法数量,这里的时间复杂度应该是O(n),n为类型的方法数,因为循环中遍历的只是v的方法集)。
4.3.5 方法调用
作为一门静态语言,如果我们想要通过reflect
包利用反射在运行期间执行方法不是一件容易的事,下面的十几行代码就使用反射来执行Add(0, 1)
函数:
func Add(a, b int) int { return a + b }
func main() {
v := reflect.ValueOf(Add)
if v.Kind() != reflect.Func {
return
}
t := v.Type()
argv := make([]reflect.Value, t.NumIn())
for i := range argv {
// 检查参数类型是否都是int
// 这里和下面的返回值个数、类型的检查感觉没必要检查,都是写死的,除非编译器出bug
if t.In(i).Kind() != reflect.Int {
return
}
argv[i] = reflect.ValueOf(i)
}
result := v.Call(argv)
if len(result) != 1 || result[0].Kind() != reflect.Int {
return
}
fmt.Println(result[0].Int()) // #=> 1
}
1.通过reflect.ValueOf
获取函数Add
对应的反射对象;
2.根据反射对象reflect.rtype.NumIn
方法返回的参数个数创建argv
数组;
3.多次调用reflect.ValueOf
函数逐一设置argv
数组中的各个参数;
4.调用反射对象Add
的reflect.Value.Call
方法并传入参数列表;
5.获取返回值数组、验证数组长度以及类型并打印其中数据;
使用反射来调用方法非常复杂,原本只需要一行代码就能完成的工作,现在需要十几行代码才能完成,但这也是在静态类型语言中使用动态特性需要付出的成本。
func (v Value) Call(in []Value) []Value {
v.mustBe(Func)
v.mustBeExported()
return v.call("Call", in)
}
reflect.Value.Call
方法是运行时调用方法的入口,它通过两个MustBe
开头的方法确定了当前反射对象的类型是函数以及可见性,随后调用reflect.Value.call
完成方法调用,这个私有方法的执行过程会分为以下几个部分:
1.检查输入参数以及类型的合法性;
2.将传入的reflect.Value
参数数组设置到栈上;
3.通过函数指针和输入参数调用函数;
4.从栈上获取函数的返回值;
我们将按照上面的顺序分析使用reflect
进行函数调用的几个过程。
参数检查
参数检查是通过反射调用方法的第一步,在参数检查期间我们会从反射对象中取出当前的函数指针unsafe.Pointer
,如果该函数指针是方法,那么我们就会通过reflect.methodReceiver
函数获取方法的接受者和函数指针。
func (v Value) call(op string, in []Value) []Value {
// 先将reflect.Value中保存的类型信息v.typ转换为funcType
// funcType是一个内部结构,用来存储函数的具体信息
t := (*funcType)(unsafe.Pointer(v.typ))
// ...
// 如果v是一个方法而非普通函数
if v.flag&flagMethod != 0 {
// 将方法接受者设为当前Value
rcvr = v
// 通过methodReceiver函数调整接受者类型、方法类型、函数指针
rcvrtype, t, fn = methodReceiver(op, v, int(v.flag)>>flagMethodShift)
} else {
// ...
}
// 验证参数数量
n := t.NumIn()
if len(in) < n {
panic("reflect: Call with too few input arguments")
}
if len(in) > n {
panic("reflect: Call with too many input arguments")
}
// 遍历每个实参
for i := 0; i < n; i++ {
// 如果实参不能赋值给形参
if xt, targ := in[i].Type(), t.In(i); !xt.AssignableTo(targ) {
panic("reflect: " + op + " using " + xt.String() + " as type " + targ.String())
}
}
在上述方法中,会检查传入参数的个数以及参数的类型与函数签名中的类型是否匹配,任何参数的不匹配都会导致整个程序的崩溃中止。
准备参数
当我们已经对当前方法的参数完成验证后,就会进入函数调用的下一阶段,为函数调用准备参数,在前面的章节函数调用
中我们已经介绍过Go语言的函数调用惯例,函数或者方法在调用时,所有的参数都会被依次放置到栈上。
// 获取函数的返回值个数
nout := t.NumOut()
// 获取函数调用时,参数和返回值的内存布局
// frametype是函数调用帧的类型
// retOffset是返回值在调用帧中的偏移量
// framePool是一个内存池,用于分配和回收函数帧内存
frametype, _, retOffset, _, framePool := funcLayout(t, rcvrtype)
// args是用于保存参数和返回值的内存
var args unsafe.Pointer
// 如果函数没有返回值
if nout == 0 {
// 从内存池中获取一个内存块,没有返回值时,可以提高效率,因为没有必须分配到堆上的返回值
args = framePool.Get().(unsafe.Pointer)
} else {
// 直接为函数帧分配新的内存
args = unsafe_New(frametype)
}
// 初始化args内存的偏移
off := uintptr(0)
// 如果有方法接受者,即这是方法而非函数
if rcvrtype != nil {
// 将方法接受者存在args内存中
storeRcvr(rcvr, args)
// 更新args内存的偏移量
off = ptrSize
}
// 遍历输入参数
for i, v := range in {
// 获取第i个参数的类型
targ := t.In(i).(*rtype)
// 获取该类型的对齐要求
a := uintptr(targ.align)
// 增加args内存的偏移量,用于对齐内存从而存放参数,它是参数的起始位置
// 对于增加后的偏移值,如果对齐要求a是8,则新off是下一个8的倍数
// 举例说明,如果a是8,相当于对off加7,然后地板除8,&^表示把最后的111都去掉
off = (off + a - 1) &^ (a - 1)
// 获取该参数的大小
n := targ.size
// ...
// 计算参数应该存储的实际地址,由于Go中指针不能做加法运算,因此需要add函数计算指针偏移
addr := add(args, off, "n > 0")
// 将参数v转换成目标参数类型targ,存放到addr处
v = v.assignTo("reflect.Value.Call", targ, addr)
// 将参数存放的位置的值设为参数的值
*(*unsafe.Pointer)(addr) = v.ptr
// 更新内存偏移的值
off += n
}
1.通过reflect.funcLayout
函数计算当前函数需要的参数和返回值的栈布局,也就是每一个参数和返回值所占的空间大小;
2.如果当前函数有返回值,需要为当前函数的参数和返回值分配一片内存空间args
;
3.如果当前函数是方法,需要将方法的接受者拷贝到args
内存中;
4.将所有函数的参数按照顺序依次拷贝到对应args
内存中:
(1)使用reflect.funcLayout
返回的参数计算参数在内存中的位置;
(2)将参数拷贝到内存空间中;
准备参数的过程是计算各个参数和返回值占用的内存空间并将所有的参数都拷贝到内存空间对应位置的过程,该过程会考虑函数和方法、返回值数量以及参数类型带来的差异。
调用函数
准备好调用函数需要的全部参数后,就会通过以下的代码执行函数指针了。我们会向该函数传入栈类型、函数指针、参数和返回值的内存空间、栈大小、返回值的偏移量:
call(frametype, fn, args, uint32(frametype.size), uint32(retOffset))
上述函数实际并不存在,它会在编译期间被链接到runtime.reflectcall
这个用汇编实现的函数上,我们在这里不会分析该函数的具体实现,感兴趣的读者可以自行了解其实现原理。
处理返回值
当函数调用结束之后,就会开始处理函数的返回值:
1.如果函数没有任何返回值,会直接清空args
中的全部内容来释放空间;
2.如果当前函数有返回值:
(1)将args
中与输入参数有关的内存空间清空;
(2)创建一个nout
长度的切片用于保存由反射对象构成的返回值数组;
(3)从函数对象中获取返回值的类型和内存大小,将args
内存中的数据转换成reflect.Value
类型并存储到切片中;
// 定义返回值数组
var ret []Value
// 如果没有返回值
if nout == 0 {
// 清空args的内存
typedmemclr(frametype, args)
// 将参数内存放回内存池中
framePool.Put(args)
// 如果函数有返回值
} else {
// 清除部分内存(参数args占用的内存)
typedmemclrpartial(frametype, args, 0, retOffset)
// 初始化返回值内存
ret = make([]Value, nout)
// 初始化用于存放返回值的内存偏移
off = retOffset
// 遍历每个返回值
for i := 0; i < nout; i++ {
// 获取第i个返回值的类型
tv := t.Out(i)
// 获取该返回值的对齐要求
a := uintptr(tv.Align())
// 更新内存偏移到下一个a的整数倍处,off现在是存放当前参数的起始位置
off = (off + a - 1) &^ (a - 1)
// 如果返回值大小非0
if tv.Size() != 0 {
// 构造该返回值的Value
fl := flagIndir | flag(tv.Kind())
ret[i] = Value{tv.common(), add(args, off, "tv.Size() != 0"), fl}
// 如果返回值大小为0
} else {
// 直接调用Zero生成零值
ret[i] = Zero(tv)
}
// 更新返回值内存偏移
off += tv.Size()
}
}
return ret
}
由reflect.Value
构成的ret
数组会被返回到上层,到这里为止使用反射实现函数调用的过程就结束了。
4.3.6 小结
Go语言的reflect
包为我们提供了多种能力,包括如何使用反射来动态修改变量、判断类型是否实现了某些接口、动态调用方法等功能,通过对反射包中方法原理的分析能帮助我们理解之前看起来比较怪异、令人困惑的现象。