为什么不建议使用init()
golangci lint
目前统一CI的.golangci.yml包含`gochecknoinits`检查器:
# golangci-lint v1.46.2# https://golangci-lint.run/usage/linterslinters:disable-all: trueenable:...- gochecknoinits # Checks that no init functions are present in Go code. ref: https://github.com/leighmcculloch/gochecknoinits...
如果go代码使用了`init()`函数,将会出现以下报错:
# golangci-lint runfoo/foo.go:3:1: don't use `init` function (gochecknoinits)func init() {^
即不建议大家使用`init()`。
为什么不建议使用init()
在google搜"golang why not use init"可以搜出一堆文章讨论使用`init()`的弊端,甚至有人建议在Go 2移除`init()`,可见这个函数的确存在不少争议。
简单总结一下,使用`init()`的问题主要有以下三个:
1). 影响代码阅读
典型代码:
import (_ "github.com/go-sql-driver/mysql")
这行代码是是让人困扰的,因为很难知道它的作用是什么。
直到打开`mysql`包代码,看到`init()`才知道原来是注册了一个`driver`:
func init() {sql.Register("mysql", &MySQLDriver{})}
试想一下,如果一个包含多个`init()`,并且分散在不同文件,阅读代码是比较痛苦的,你必须把所有`init()`找出来才能了解在初始化的时候做了什么。起码现在的IDE是不会帮你把所有`init()`汇总起来的。
2). 影响单元测试
看下面一段代码:
package fooimport ("os")var myFile *os.Filefunc init() {var err errormyFile, err = os.OpenFile("f.txt", os.O_RDWR, 0755)if err != nil {panic(err)}}func bar(a, b int) int {return a + b}
想对`bar()`函数写单元测试:
package fooimport "testing"func Test_bar(t *testing.T) {_ = bar(1, 2)}
很大概率是无法跑通的,因为在`init()`已经触发`panic`了。
调用方无法控制`init()`的执行,所以必须想办法让其运行正确(如先把f.txt创建了?),这时候就比较难写单元测试了。
3). 难以处理error
从`init()`的函数签名可以看到,它无法`return error`。那么如果函数题内发生error,如何处理?
方式一: panic
不少知名的包都会这么写,如`promethues`:
func init() {MustRegister(NewProcessCollector(ProcessCollectorOpts{})) //Must函数会抛出panicMustRegister(NewGoCollector())}
还有`gorm`:
var gormSourceDir stringfunc init() {_, file, _, _ := runtime.Caller(0)gormSourceDir = regexp.MustCompile(`utils.utils\.go`).ReplaceAllString(file, "") //Must函数会抛出panic}
在`init()`抛出`panic`的最大问题是要如何让调用方知道。因为很可能只是`import`了这个包就触发`panic`了。
方式二: 定义initError
除了直接`panic`,还可以定义一个包级error,但这种写法相对较少,如下:
var (myFile *os.FileopenFileErr error)func init() {myFile, openFileErr = os.OpenFile("f.txt", os.O_RDWR, 0755)}func GetOpenFileErr() error {return openFileErr}
调用方使用`GetOpenFileErr()`即可知道初始化有没有失败。
这种写法存在的问题是,`init()`只能执行一次,如果发生error后想再执行就没办法了。
使用init()的时机
首先可以明确的是:业务代码不建议使用`init()`,库代码(如common-library)可适度使用`init()`。
Code Review的人需要注意`init()`是否有被滥用,如定义了多个`init()`,或`init()`函数逻辑过于复杂,如在`init()`请求一个第三方接口,接口有可能挂掉。
init()的代替方案
1). 简单的包级变量初始化,可以直接赋值,无需写`init()`
Bad:
var (a []intb map[string]string)func init() {a = []int{1, 2, 3, 4, 5}b = map[string]string{"a": "a", "b": "b"}}
Good:
var (a = []int{1, 2, 3, 4, 5}b = map[string]string{"a": "a", "b": "b"})
或者:
var a = function() []int{ return xxxxx }()参考"redigo"包:
var (sentinel []bytesentinelOnce sync.Once)func initSentinel() {p := make([]byte, 64)if _, err := rand.Read(p); err == nil {sentinel = p} else {h := sha1.New()io.WriteString(h, "Oops, rand failed. Use time instead.")io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10))sentinel = h.Sum(nil)}}...//调用sentinelOnce.Do(initSentinel)...
3).如果想处理error,并且发生error后可重复初始化,可以加把锁:
var (myFile *os.Filemu sync.RWMutex //跟sync.Once类似,但能在发生error时重新执行)func initFile() error {mu.RLock()if myFile != nil {mu.RUnlock()return nil}mu.RUnlock()mu.Lock()defer mu.Unlock()if myFile != nil {return nil}var err errormyFile, err = os.OpenFile("f.txt", os.O_RDWR, 0755)return err}
总结
1). 使用`init()`有利有弊,有时候能把代码写得更简洁,但滥用会带来"Code Smell";
2). 业务代码不建议使用`init()`,common-library谨慎使用;
3). 业务代码尽量避免使用包级变量,如需使用,简单初始化可直接赋值,复杂初始化可自定义`init函数`+sync.Once。