目前统一CI的.golangci.yml包含`gochecknoinits`检查器:
# golangci-lint v1.46.2# https://golangci-lint.run/usage/linterslinters: disable-all: true enable: ... - 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()`。
在google搜"golang why not use init"可以搜出一堆文章讨论使用`init()`的弊端,甚至有人建议在Go 2移除`init()`,可见这个函数的确存在不少争议。
简单总结一下,使用`init()`的问题主要有以下三个:
典型代码:
import ( _ "github.com/go-sql-driver/mysql" )
这行代码是是让人困扰的,因为很难知道它的作用是什么。
直到打开`mysql`包代码,看到`init()`才知道原来是注册了一个`driver`:
func init() { sql.Register("mysql", &MySQLDriver{})}
试想一下,如果一个包含多个`init()`,并且分散在不同文件,阅读代码是比较痛苦的,你必须把所有`init()`找出来才能了解在初始化的时候做了什么。起码现在的IDE是不会帮你把所有`init()`汇总起来的。
看下面一段代码:
package foo import ( "os") var myFile *os.File func init() { var err error myFile, 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 foo import "testing" func Test_bar(t *testing.T) { _ = bar(1, 2)}
很大概率是无法跑通的,因为在`init()`已经触发`panic`了。
调用方无法控制`init()`的执行,所以必须想办法让其运行正确(如先把f.txt创建了?),这时候就比较难写单元测试了。
从`init()`的函数签名可以看到,它无法`return error`。那么如果函数题内发生error,如何处理?
不少知名的包都会这么写,如`promethues`:
func init() { MustRegister(NewProcessCollector(ProcessCollectorOpts{})) //Must函数会抛出panic MustRegister(NewGoCollector())}
还有`gorm`:
var gormSourceDir string func init() { _, file, _, _ := runtime.Caller(0) gormSourceDir = regexp.MustCompile(`utils.utils\.go`).ReplaceAllString(file, "") //Must函数会抛出panic}
在`init()`抛出`panic`的最大问题是要如何让调用方知道。因为很可能只是`import`了这个包就触发`panic`了。
除了直接`panic`,还可以定义一个包级error,但这种写法相对较少,如下:
var ( myFile *os.File openFileErr error) func init() { myFile, openFileErr = os.OpenFile("f.txt", os.O_RDWR, 0755)} func GetOpenFileErr() error { return openFileErr}
调用方使用`GetOpenFileErr()`即可知道初始化有没有失败。
这种写法存在的问题是,`init()`只能执行一次,如果发生error后想再执行就没办法了。
首先可以明确的是:业务代码不建议使用`init()`,库代码(如common-library)可适度使用`init()`。
Code Review的人需要注意`init()`是否有被滥用,如定义了多个`init()`,或`init()`函数逻辑过于复杂,如在`init()`请求一个第三方接口,接口有可能挂掉。
1). 简单的包级变量初始化,可以直接赋值,无需写`init()`
Bad:
var ( a []int b 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 }()
2).如果初始化比较复杂,可以使用"自定义init"+sync.Once,实现延迟初始化:
参考"redigo"包:
var ( sentinel []byte sentinelOnce 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.File mu 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 error myFile, err = os.OpenFile("f.txt", os.O_RDWR, 0755) return err}
1). 使用`init()`有利有弊,有时候能把代码写得更简洁,但滥用会带来"Code Smell";
2). 业务代码不建议使用`init()`,common-library谨慎使用;
3). 业务代码尽量避免使用包级变量,如需使用,简单初始化可直接赋值,复杂初始化可自定义`init函数`+sync.Once。
作者:万梓荣
来源-微信公众号:三七互娱技术团队
出处
:https://mp.weixin.qq.com/s/Q8MiewY_hDY7SAQKBc8XvA