golangci lint
Currently, the unified CI's .golangci.yml includes the gochecknoinits
checker:
# 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
...
If Go code uses the init()
function, the following error will occur:
# golangci-lint run
foo/foo.go:3:1: don't use `init` function (gochecknoinits)
func init() {
^
That is, it's not recommended to use init()
.
Why not to use init()
Searching "golang why not use init" on Google will yield a bunch of articles discussing the drawbacks of using init()
, with some even suggesting its removal in Go 2, indicating considerable controversy surrounding this function.
To summarize briefly, the main problems with using init()
are:
Affecting code readability
Typical code seen in some program:
import (
_ "github.com/go-sql-driver/mysql"
)
This line of code is perplexing because it's hard to discern its purpose.
It's only upon opening the mysql
package code and seeing init()
that one realizes it's registering a driver
:
func init() {
sql.Register("mysql", &MySQLDriver{})
}
Imagine a scenario where there are multiple init()
scattered across different files; reading the code becomes quite painful as you must locate all init()
functions to understand what's being initialized. At least current IDEs won't aggregate all init()
functions for you.
Impacting unit testing
Consider the following code:
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
}
Attempting to write unit tests for the bar()
function:
package foo
import "testing"
func Test_bar(t *testing.T) {
_ = bar(1, 2)
}
Chances are high that it won't run successfully because panic
is already triggered in init()
.
The caller cannot control the execution of init()
, so one must figure out how to make it run correctly (like creating f.txt
first?), making it difficult to write unit tests.
Handling errors is challenging
From the function signature of init()
, it's evident that it cannot return error
. So, how do you handle errors within the function?
Option 1: panic
Many well-known packages are written this way, like promethues:
func init() {
MustRegister(NewProcessCollector(ProcessCollectorOpts{})) //Must will give panic
MustRegister(NewGoCollector())
}
And gorm:
var gormSourceDir string
func init() {
_, file, _, _ := runtime.Caller(0)
gormSourceDir = regexp.MustCompile(`utils.utils\.go`).ReplaceAllString(file, "")
}
The main issue with panic
in init()
is how to let the caller know because merely importing this package might trigger a panic
.
Option 2: Define initError
Apart from directly panic
-ing, one can define a package-level error, though this method is relatively less common, like below:
var (
myFile *os.File
openFileErr error
)
func init() {
myFile, openFileErr = os.OpenFile("f.txt", os.O_RDWR, 0755)
}
func GetOpenFileErr() error {
return openFileErr
}
Callers can use GetOpenFileErr()
to know whether initialization failed.
The problem with this approach is that init()
can only be executed once, so if an error occurs and you want to re-execute, there's no way.
Timing of using init()
First and foremost, it's clear: business code should avoid using init(), while library code (e.g., common-library) can use it moderately.
Code reviewers need to watch out for abuse of init()
, such as defining multiple init()
or init()
functions with overly complex logic, like making a request to a third-party API, which might crash.
Alternatives to init()
1). Simple package-level variable initialization can be directly assigned without needing 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"}
)
or:
var a = function() []int{ return xxxxx }()
2). If initialization is complex, "custom init" + sync.Once
can be used for lazy initialization:
Refer to the redigo package:
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)
}
}
...
//call
sentinelOnce.Do(initSentinel)
...
3). If error handling and repeated initialization after error occurrence are desired, a lock can be added:
var (
myFile *os.File
mu sync.RWMutex
)
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
}
Summary
- Using
init()
has its pros and cons; sometimes it can make the code more concise, but overuse can lead to "Code Smell". - Business code should avoid using
init()
, while common-library should use it cautiously. - Business code should try to avoid using package-level variables, but if needed, simple initialization can be done by direct assignment, and complex initialization can be achieved with a custom init function +
sync.Once
.