不能使用 init() 吗

最近一段时间,较多改用 golangci-lint 工具。这是因为 golint 已经停止维护了,所以面对不断释出的 golang 新版本,就必须对各类辅助工具同步进行更新。比如说 gofmt 时不时有点不妥了,gocyclo 认不得泛型语法了,等等。很多工具目前尚未有新版本发放,GoLand 也总是爱崩溃了,所以面对 go 1.18 来讲,问题还很多。

言归正传,使用新的 lint 工具,很多报错,基本上都是 Effective Go 一文中提及的各类不良味道。

其中 gochecknoinits 错误就是一条。

报错是这样的:

1
2
3
4
diff/v/a.go:3:1: don't use `init` function (gochecknoinits)
func init() {
^

对应的源码是这样的:

1
2
3
4
5
6
7
package v

func init() {
	println("a.init")
}

var A int

消除这个错误

使用 nolint 注释可以消除这个错误:

1
2
3
4
5
6
7
package v

func init() { //nolint:gochecknoinits
	println("a.init")
}

var A int

研究这个错误

那么,它是不是真的是个错误呢?

对于我们的代码,在 init 函数中做一些全局性的初始化,令其集中到一处,有若干好处:

  1. 比较于全局变量在声明处就地赋值来说,init() 能够正确约束初始化顺序,这一点有时候是我们所必需的
  2. init 能够提供复杂的初始化逻辑,从而不必受制于变量声明并赋值的语法
  3. 在变量声明时使用一个函数调用来给付初值,有可能是另一种滥用:你可以会不能觉察到沉重的初始化延迟是来自于不恰当的初值构造。但如果使用 init 的话,通常更容易警醒于此。

这些好处不见得是必然的、必需的,我们总是可以有多种方法来改写代码越过限制。

所以说 init() 如此的有用,怎么就成为了 bad smell 了呢?

反对者们是这么说的:

因为 init() 的用途本质上就是初始化全局变量,而良好的代码习惯是尽可能别要用全局变量,所以顺理成章地也就不应该使用 init()。这不难理解,对吗?

为何不应该使用全局变量呢?按照 Golang 创始人之一的说法是,避免边际效应,不要使用包级别的全局变量。

- no side effect imports - no package level variables

所谓边际效应,其实是指全局变量被在 init 中被以各种潜在可能的方法进行初始化,这可能会导致变量的值被意外地设定到非预期的状态。这一点比较麻烦,得是要多么无理才会写出那么奇葩的代码序列吖。然而,Deve Cheney 的话也并不尽然是危言耸听。

此外,由于变量的初始化被转移到了 init 函数体中(甚至于进一步地迁移到某些子函数中),它的声明处的代码就无法体现出正确的变量状态,你必须在整个包中翻拣全部的 init 函数,以及全部相关的子函数调用,才能查证到该变量究竟会得到一个什么样的初值。从这个结果来说,把 init 看作是不良代码习惯也不算太冤枉。

话说回来,用对了,就是好刀;用错了,难免就会割到手。

结论

所以对于终端开发者来说,避免 init 基本上是正确的态度。顺便,也避免滥用全局变量。

但是对于库作者来说,嗯,是的,说的就是我自己,那么该用就用,最多不过加条 nolint 注释也没什么大不了的。

Why?

库作者知道自己在干什么,也知道自己该干什么,所以他有权利变废为宝。

后话

除了 gochecknoinits 之外,还有一个 gochecknoglobals ,大家是差不多的,就不再另文了。

另外,更恰当的消除方案是配置一个久经考验的 golangci 配置文件。这一方案,或许我会在以后的文章中提出一份参考,目前来说先放一放吧。

REFs

🔚

留下评论