Golang中避免使用init()函数的原因

发表时间: 2024-05-09 11:30

golangci lint


目前统一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()`。

为什么不建议使用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