Why init() is not recommended in Go

Summary

Go's init() function is generally not recommended, particularly for business logic, due to significant drawbacks impacting code readability, unit testing, and error handling. Its implicit execution makes understanding program flow difficult and prevents direct control during tests, often forcing panics for errors as it cannot return a value. Linters like gochecknoinits actively discourage its use. Instead, developers should prefer direct variable assignments for simple initializations or implement custom functions with sync.Once for complex, lazy setups, potentially incorporating locks for robust error handling and re-initialization. While library code might use init() cautiously, it should largely be avoided in application code.

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

  1. Using init() has its pros and cons; sometimes it can make the code more concise, but overuse can lead to "Code Smell".
  2. Business code should avoid using init(), while common-library should use it cautiously.
  3. 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.
GOLANG INIT() SYNC.ONCE PANIC

  RELATED

  COMMENTS

0

No comment for this article.