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.

37 Interactive Technology Team
37 Interactive Technology Team
37 Interactive Technology Team
Why Avoid Go’s init() Function? Risks, Testing Issues, and Better Alternatives

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.

backendTestingGobest practicescode qualityError Handlinginit
37 Interactive Technology Team
Written by

37 Interactive Technology Team

37 Interactive Technology Center

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.