Why Avoid Go’s init() Function? Risks, Testing Issues, and Better Alternatives
This article explains why using Go's init() function is discouraged due to readability, testing complications, and error‑handling limitations, and it offers practical alternatives such as direct variable initialization, custom init functions with sync.Once, and controlled error‑aware initialization patterns.
The golangci-lint configuration commonly enables the gochecknoinits linter, which reports any init() function in Go source files, e.g.:
# golangci-lint run
foo/foo.go:3:1: don't use `init` function (gochecknoinits)
func init() {}Why Avoid Using init()
1. Reduces Code Readability
Packages are often imported solely for side‑effects:
import (
_ "github.com/go-sql-driver/mysql"
)The import does not reveal what the side‑effect is. The actual registration occurs inside an init() function inside the imported package, which may be scattered across multiple files. Developers and IDEs must locate every init() to understand the initialization flow.
2. Complicates Unit Testing
When an init() performs I/O and panics on error, the panic happens before any test code runs, making the test impossible to execute without external setup:
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 }A test that calls bar() will fail if the panic in init() triggers, because the caller cannot control when or whether init() runs.
3. No Direct Error Propagation
The signature of init() does not return an error, so failures must be handled by panicking or by storing a package‑level error variable.
Common Patterns for Handling Errors in init()
Approach A – Panic
Many libraries (e.g., prometheus, gorm) call panic inside init() when a required resource cannot be created. The downside is that importing the package may immediately terminate the program, and the caller receives no explicit context.
Approach B – Package‑Level Error Variable
A less common pattern defines a global error and provides an accessor:
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 query GetOpenFileErr() to detect initialization failure, but the init() runs only once, so retrying after an error is impossible.
When to Use init()
Business (application) code should avoid init(). Library code may use it sparingly, but reviewers should watch for multiple init() functions or complex logic (e.g., network calls) that can cause hidden side‑effects.
Recommended Alternatives to init()
Direct Package‑Level Variable Initialization Assign values at declaration time instead of using an init() function:
var (
a = []int{1, 2, 3, 4, 5}
b = map[string]string{"a": "a", "b": "b"}
)Lazy Initialization with sync.Once For expensive or order‑dependent setup, define a separate initializer and guard it with sync.Once so it runs at most once, on first use:
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)
}
}
// Usage example
sentinelOnce.Do(initSentinel)Retryable Initialization Protected by a Mutex If initialization may fail and needs to be attempted again, protect the logic with a read‑write mutex. The pattern checks whether the resource is already initialized, acquires an exclusive lock only when necessary, and returns any error to the caller:
var (
myFile *os.File
mu sync.RWMutex // similar to sync.Once but allows retry on error
)
func initFile() error {
mu.RLock()
if myFile != nil {
mu.RUnlock()
return nil
}
mu.RUnlock()
mu.Lock()
defer mu.Unlock()
if myFile != nil { // double‑check after acquiring write lock
return nil
}
var err error
myFile, err = os.OpenFile("f.txt", os.O_RDWR, 0755)
return err
}Summary
While init() can reduce boilerplate, overusing it creates a code smell: it obscures initialization logic, hinders unit testing, and forces error handling through panics or global variables. Prefer direct variable initialization for simple cases, lazy initialization guarded by sync.Once for one‑time expensive setup, or mutex‑protected functions when retryable error handling is required. Business code should generally avoid init(); library code may use it cautiously with clear documentation and minimal side‑effects.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
