Why init() is not recommended in Go

  sonic0002        2024-05-10 07:46:18       2,879        0    

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


  0 COMMENT


No comment for this article.



  RANDOM FUN

What is procedure programming?

After viewing this picture, you should understand what procedure programming is. Want to install air conditioner at higher levels? Give them more ladders.