GoLang之包装方法系列二
1.前言
上一篇中我们发现了编译器会为接收者为值类型的方法生成接收者为指针类型的包装方法,并且知道这些包装方法主要是为了支持接口。但是如果反编译或者用nm命令来分析可执行文件的话,就会发现:
不只是这些包装方法,就连代码中的原始方法也不一定会存在于可执行文件中。
道理其实很简单,链接器在生成可执行文件的时候,会对所有OBJ文件中的函数、方法、以及类型元数据等进行统计分析,对于那些确定没有用到数据,链接器会直接将其裁剪掉,以优化最终可执行文件的大小。
看起来一切顺理成章,但是又有一个问题,反射是在运行阶段工作的,通过反射还可以调用方法,那么链接器是如何保证不把反射要用的方法给裁剪掉的呢?
于是笔者就做了一个小小的实验。
2.实验1:编译如下代码
type Number float64
func (n Number) IntValue() int {
return int(n)
}
func main() {
n := Number(9)
v := reflect.ValueOf(n)
_ = v
}
编译如代码后用nm命令分析得到的可执行文件,结果发现IntValue方法被裁剪掉了:
$ go tool nm the-types.exe | grep Number
3.实验2:对main函数稍作修改
func main() {
n := Number(9)
v := reflect.ValueOf(n)
v.MethodByName("")
_ = v
}
再次编译并用nm命令检查:
$ go tool nm the-types.exe | grep Number
48f0c0 T main.(*Number).IntValue
48efa0 T main.Number.IntValue
这次IntValue的两个方法都被保留了下来,如果换成v.Method(0)也能达到同样的效果。
也就是说链接器裁剪的时候会检查用户代码是否会通过反射来调用方法,会的话就把该类型的方法保留下了,只有在明确确认这些方法在运行阶段不会被用到时,才可以安全的裁剪。
4.实验3:再次修改main函数代码
func main() {
n := Number(9)
var a interface{} = n
println(a)
v := reflect.ValueOf("")
v.MethodByName("")
}
发现这种情况下Number的两个方法依旧被保留了下来,从代码逻辑来看,运行阶段是不可能会用到Number的方法的。
5.实验4:再把main函数修改一下
func main() {
n := Number(9)
println(n)
v := reflect.ValueOf("")
v.MethodByName("")
}
这次有所不同,Number的两个方法被裁剪掉了。由此可以总结出反射影响方法裁剪的两个必要条件:
一是代码中存在从目标类型到接口类型的赋值操作,因为运行阶段类型信息萃取始于接口;
二是代码中调用了MethodByName、Method这些方法。因为代码中有太多灵活的逻辑,编译阶段的分析无法做到尽如人意。
论——实验的重要性!
6.总结
通过对包装方法的深度探索,我们已经明确了以下几个问题:
1.什么是包装方法;
2.生成包装方法主要是为了支持接口;
3.包装方法会经过链接器裁剪,不一定会存在于可执行文件中。
如果理解了上述问题,请试着回答下面这个问题:
为啥Go不允许为T和*T定义同名方法?