Go 语言反射的实现原理( 四 )

运行上述代码我们会发现 CustomError 类型并没有实现 error 接口,而 *CustomError 指针类型却实现了接口,这其实也比较好理解,我们在 接口 一节中也介绍过可以使用结构体和指针两种不同的类型实现接口 。
?复制代码
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") } return implements(u.(*rtype), t)}Implements 方法会检查传入的类型是不是接口,如果不是接口或者是空值就会直接 panic 中止当前程序,否则就会调用私有的函数 implements 判断类型之间是否有实现关系:
?复制代码
func implements(T, V *rtype) bool { t := (*interfaceType)(unsafe.Pointer(T)) if len(t.methods) == 0 { return true } // ... v := V.uncommon() i := 0 vmethods := v.methods() for j := 0; j < int(v.mcount); j++ { tm := &t.methods[i] tmName := t.nameOff(tm.name) vm := vmethods[j] vmName := V.nameOff(vm.name) if vmName.name() == tmName.name() && V.typeOff(vm.mtyp) == t.typeOff(tm.typ) { if i++; i >= len(t.methods) { return true } } } return false}如果接口中不包含任何方法,也就意味着这是一个空的 interface{},任意的类型都可以实现该协议,所以就会直接返回 true 。

Go 语言反射的实现原理

文章插图
 
在其他情况下,由于方法是按照一定顺序排列的,implements 中就会维护两个用于遍历接口和类型方法的索引 i 和 j,所以整个过程的实现复杂度是 O(n+m),最多只会进行 n + m 次数的比较,不会出现次方级别的复杂度 。
方法调用
作为一门静态语言,如果我们想要通过 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 { 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. 根据反射对象 NumIn 方法返回的参数个数创建 argv 数组;
  3. 多次调用 reflect.Value 逐一设置 argv 数组中的各个参数;
  4. 调用反射对象 Add 的 Call 方法并传入参数列表;
  5. 获取返回值数组、验证数组的长度以及类型并打印其中的数据;
使用反射来调用方法非常复杂,原本只需要一行代码就能完成的工作,现在需要 10 多行代码才能完成,但是这也是在静态语言中使用这种动态特性需要付出的成本,理解这个调用过程能够帮助我们深入理解 Go 语言函数和方法调用的原理 。
?复制代码
func (v Value) Call(in []Value) []Value { v.mustBe(Func) v.mustBeExported() return v.call("Call", in)}Call 作为反射包运行时调用方法的入口,通过两个 MustBe 方法保证了当前反射对象的类型和可见性,随后调用 call 方法完成运行时方法调用的过程,这个过程会被分成以下的几个部分:
  1. 检查输入参数的合法性以及类型等信息;
  2. 将传入的 reflect.Value 参数数组设置到栈上;
  3. 通过函数指针和输入参数调用函数;
  4. 从栈上获取函数的返回值;
我们将按照上面的顺序依次详细介绍使用 reflect 进行函数调用的几个过程 。
参数检查
参数检查是通过反射调用方法的第一步,在参数检查期间我们会从反射对象中取出当前的函数指针 unsafe.Pointer,如果待执行的函数是方法,就会通过 methodReceiver 函数获取方法的接受者和函数指针 。
?复制代码
func (v Value) call(op string, in []Value) []Value { t := (*funcType)(unsafe.Pointer(v.typ)) var ( fn unsafe.Pointer rcvr Value rcvrtype *rtype ) if v.flag&flagMethod != 0 { rcvr = v rcvrtype, t, fn = methodReceiver(op, v, int(v.flag)>>flagMethodShift) } else if v.flag&flagIndir != 0 { fn = *(*unsafe.Pointer)(v.ptr) } else { fn = v.ptr } 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()) } } nin := len(in) if nin != t.NumIn() { panic("reflect.Value.Call: wrong argument count") }


推荐阅读