老弟有空吗,我Go项目里某个init函数被调用了两次!

周五快要下班了,同事过来找我,说他的Go项目里某个init函数被调用了两次。WTF,这不耽误回家过周末吗!
废话不多说,直接上同事工位看现场。

同事向我展示了两行内容相同的日志,并且与之对应的源码。大概像下面这样:

1
2
3
func init() {
log.Info("xxx")
}

我让同事将代码中的日志稍微做了些修改。大概像下面这样:

1
2
3
4
func init() {
//log.Info("xxx")
log.Info("yyy")
}

重新运行,打印出的日志变成了两行yyy,说明日志确实是由该处产生的。

我怀疑是init函数被手动调用了。由于init函数是小写开头的,包外应该调用不了,所以我让同事在包内搜索是否有手动调用的地方,结果没有。
(事后实验证明,Go不允许手动调用init函数,会产生编译错误)。

于是我又想了想,Go中的init函数会否由于多次import造成多次调用。应该是不会,不然日常很多代码都应该有问题。

由于同事编写代码的环境,编译环境,运行环境可能不在一台机器上,我怀疑是中途某个环节同步代码时出错了。

所以我将代码再次做了些修改。大概像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
// 声明一个包(全局)作用域的变量
var tmp int

func init() {
//log.Info("yyy")

// 此处,我没再用代码中引入的第三方log库,而是使用标准库中的log库,目的是将源码文件打印出来
log.SetFlags(log.Llongfile)
// 看两次打印的tmp值,以及对应的地址
log.Printf("zzz %d %p", tmp, &tmp)
tmp = 10
}

打印出的结果显示,两次tmp的值都是0,并且地址是不同的。
更为关键的是,打印出的源码文件是不同的!大概像下面这样:
github.com/test/x/config.gogithub.com/test2/x/config.go

到此问题基本定位到了,反馈给同事,同事说项目的git路径确实发生过变化,由test变成了test2,于是他做了个软链。。
那么原因也找到了,应该是代码中同时import了testtest2导致的。
至于具体是哪个地方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/

0%