周五快要下班了,同事过来找我,说他的Go项目里某个init函数被调用了两次。WTF,这不耽误回家过周末吗!
废话不多说,直接上同事工位看现场。
同事向我展示了两行内容相同的日志,并且与之对应的源码。大概像下面这样:
1 | func init() { |
我让同事将代码中的日志稍微做了些修改。大概像下面这样:
1 | func init() { |
重新运行,打印出的日志变成了两行yyy
,说明日志确实是由该处产生的。
我怀疑是init函数被手动调用了。由于init函数是小写开头的,包外应该调用不了,所以我让同事在包内搜索是否有手动调用的地方,结果没有。
(事后实验证明,Go不允许手动调用init函数,会产生编译错误)。
于是我又想了想,Go中的init函数会否由于多次import造成多次调用。应该是不会,不然日常很多代码都应该有问题。
由于同事编写代码的环境,编译环境,运行环境可能不在一台机器上,我怀疑是中途某个环节同步代码时出错了。
所以我将代码再次做了些修改。大概像下面这样:
1 | // 声明一个包(全局)作用域的变量 |
打印出的结果显示,两次tmp的值都是0,并且地址是不同的。
更为关键的是,打印出的源码文件是不同的!大概像下面这样:github.com/test/x/config.go
和github.com/test2/x/config.go
到此问题基本定位到了,反馈给同事,同事说项目的git路径确实发生过变化,由test
变成了test2
,于是他做了个软链。。
那么原因也找到了,应该是代码中同时import了test
和test2
导致的。
至于具体是哪个地方import的,我就不关心了。
向同事说明,Go中以package为单位管理代码,两个不同路径的package,即使代码完全相同,依然是两个package。
当有依赖全局状态时,可能就会出现问题。比如同事的业务代码就不允许init被调用两次,根本原因,也基本能猜到是有全局状态。
或者换个角度理解,我添加的代码中,出现了两个tmp可能就会隐藏某些bug。
闪人,回家过周末。
回家的路上,我又想了想,定位这个问题是否有更快的手段。
可以在init函数中把调用堆栈打印出来,但是init函数的调用应该是编译器生成的,在main入口处调用,由于我确认了init函数无法手动调用,所以打堆栈基本也没什么卵用。
使用go list将项目依赖的所有包打印出来,这个方法应该也行得通。
但是前文也说了,由于他们的项目,开发、编译、运行可能不是一个机器,甚至编译时的go命令都是经过封装过的工具,我也绕不清。也就不好去go list。理论上环境单纯的话,在编译机go list也是可以的。
不过这也有点事后诸葛亮,事实上,我最后定位到问题时用的方法,我事先也没有明确猜到是import两处相同代码引起的,我只是隐约怀疑代码在编译过程中发生了拷贝。
本文完,作者yoko,尊重劳动人民成果,转载请注明原文出处: https://pengrl.com/p/20030/