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

除此之外,在参数检查的过程中我们还会检查当前传入参数的个数以及所有参数的类型是否能被传入该函数中,任何参数不匹配的问题都会导致当前函数直接 panic 并中止整个程序 。
准备参数
当我们已经对当前方法的参数验证完成之后,就会进入函数调用的下一个阶段,为函数调用准备参数,在前面的章节 函数调用 中我们已经介绍过 Go 语言的函数调用的惯例,所有的参数都会被依次放置到堆栈上 。
?复制代码
nout := t.NumOut() frametype, _, retOffset, _, framePool := funcLayout(t, rcvrtype) var args unsafe.Pointer if nout == 0 { args = framePool.Get().(unsafe.Pointer) } else { args = unsafe_New(frametype) } off := uintptr(0) if rcvrtype != nil { storeRcvr(rcvr, args) off = ptrSize } for i, v := range in { targ := t.In(i).(*rtype) a := uintptr(targ.align) off = (off + a - 1) &^ (a - 1) n := targ.size if n == 0 { v.assignTo("reflect.Value.Call", targ, nil) continue } addr := add(args, off, "n > 0") v = v.assignTo("reflect.Value.Call", targ, addr) if v.flag&flagIndir != 0 { typedmemmove(targ, addr, v.ptr) } else { *(*unsafe.Pointer)(addr) = v.ptr } off += n }

  1. 通过 funcLayout 函数计算当前函数需要的参数和返回值的堆栈布局,也就是每一个参数和返回值所占的空间大小;
  2. 如果当前函数有返回值,需要为当前函数的参数和返回值分配一片内存空间 args;
  3. 如果当前函数是方法,需要向将方法的接受者拷贝到 args 这片内存中;
  4. 将所有函数的参数按照顺序依次拷贝到对应 args 内存中使用 funcLayout 返回的参数计算参数在内存中的位置;通过 typedmemmove 或者寻址的放置拷贝参数;
    准备参数的过程其实就是计算各个参数和返回值占用的内存空间,并将所有的参数都拷贝内存空间对应的位置上 。
调用函数
准备好调用函数需要的全部参数之后,就会通过以下的表达式开始方法的调用了,我们会向该函数中传入栈类型、函数指针、参数和返回值的内存空间、栈的大小以及返回值的偏移量:
?复制代码
call(frametype, fn, args, uint32(frametype.size), uint32(retOffset))这个函数实际上并不存在,它会在编译期间被链接到 runtime.reflectcall 这个用汇编实现的函数上,我们在这里并不会展开介绍该函数的具体实现,感兴趣的读者可以自行了解其实现原理 。
处理返回值
当函数调用结束之后,我们就会开始处理函数的返回值了,如果函数没有任何返回值我们就会直接清空 args 中的全部内容来释放内存空间,不过如果当前函数有返回值就会进入另一个分支:
?复制代码
var ret []Value if nout == 0 { typedmemclr(frametype, args) framePool.Put(args) } else { typedmemclrpartial(frametype, args, 0, retOffset) ret = make([]Value, nout) off = retOffset for i := 0; i < nout; i++ { tv := t.Out(i) a := uintptr(tv.Align()) off = (off + a - 1) &^ (a - 1) if tv.Size() != 0 { fl := flagIndir | flag(tv.Kind()) ret[i] = Value{tv.common(), add(args, off, "tv.Size() != 0"), fl} } else { ret[i] = Zero(tv) } off += tv.Size() } } return ret}
  1. 将 args 中与输入参数有关的内存空间清空;
  2. 创建一个 nout 长度的切片用于保存由反射对象构成的返回值数组;
  3. 从函数对象中获取返回值的类型和内存大小,将 args 内存中的数据转换成 reflect.Value 类型的返回值;
由 reflect.Value 构成的 ret 数组最终就会被返回到上层,使用反射进行函数调用的过程也就结束了 。
总结
我们在这一节中 Go 语言的 reflect 包为我们提供的多种能力,其中包括如何使用反射来动态修改变量、判断类型是否实现了某些协议以及动态调用方法,通过对反射包中方法原理的分析帮助我们理解之前看起来比较怪异、令人困惑的现象 。




推荐阅读