专业编程基础技术教程

网站首页 > 基础教程 正文

Golang为什么不建议使用init()(golang用什么ide)

ccvgpt 2024-07-21 17:43:16 基础教程 16 ℃

golangci lint


目前统一CI的.golangci.yml包含`gochecknoinits`检查器:

Golang为什么不建议使用init()(golang用什么ide)

# 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

Tags:

最近发表
标签列表