golangci lint
目前统一CI的.golangci.yml包含`gochecknoinits`检查器:
# golangci-lint v1.46.2
# https://golangci-lint.run/usage/linters
linters:
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 run
foo/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 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创建了?),这时候就比较难写单元测试了。
3). 难以处理error
从`init()`的函数签名可以看到,它无法`return error`。那么如果函数题内发生error,如何处理?
方式一: panic
不少知名的包都会这么写,如`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`了。
方式二: 定义initError
除了直接`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()的时机
首先可以明确的是:业务代码不建议使用`init()`,库代码(如common-library)可适度使用`init()`。
Code Review的人需要注意`init()`是否有被滥用,如定义了多个`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