Skip to content

Go init() Function — Senior Level

1. Overview

At senior level, init is no longer a feature you use. It is a piece of the runtime/compiler contract you understand. Concretely, you know: - How the compiler renames each init to a unique symbol (init.0, init.1, ...) and emits a per-package wrapper. - How package-level variable initialization is ordered as a topological sort over the dependency graph (with deterministic tie-breaking). - How runtime.doInit walks the package list and runs each package's wrapper exactly once. - How a stack of firstmoduledata describes the linker-defined ordering of all packages and their init bodies. - Why init cycles are detected at compile time vs at runtime. - How tests reuse the same init pipeline (with init called for _test packages too).

This document opens the box.


2. Compiler Emission

2.1 Renaming

Inside the compiler, every func init() body becomes a uniquely named function. Historically these have been named init.0, init.1, ... within a package. The compiler also synthesizes a per-package wrapper, traditionally init (the package-level wrapper).

You can see them in the binary's symbol table:

go tool objdump -s 'mypkg\.init' ./yourbin

You'll see entries like:

TEXT mypkg.init.0(SB)
TEXT mypkg.init.1(SB)
TEXT mypkg.init(SB)

The wrapper mypkg.init is the entry point the runtime calls. It in turn: 1. Initializes any package-level variables that need code (constants and simple literals are baked into the data section; complex initializers are computed here). 2. Calls each init.N in compile-determined order.

2.2 Where in the Compiler

Relevant source paths in the Go tree: - src/cmd/compile/internal/pkginit/init.go — generates the per-package init wrapper, including the package-var initialization code and the chained calls to user init functions. - src/cmd/compile/internal/pkginit/initorder.go — performs the dependency analysis among package-level vars and produces a deterministic ordering. - src/cmd/compile/internal/typecheck/dcl.go — declares and validates each init body during type-checking. - src/cmd/compile/internal/noder/... — surface-level handling of func init() declarations from source.

2.3 Per-Package init Wrapper

Conceptually, the wrapper looks like:

package mypkg

// generated by the compiler:
var initdone uint8
func init() {
    if initdone == 2 { return }
    if initdone == 1 { panic("init cycle") }
    initdone = 1
    // run dependency packages' inits (the linker generates the call list)
    importpkg1.init()
    importpkg2.init()
    // initialize package-level vars in topo order
    var0 = expr0()
    var1 = expr1(var0)
    // run user init functions in source/file order
    init·0()
    init·1()
    initdone = 2
}

Modern Go uses a global inittask table rather than per-package state vars, but the semantics are the same.

2.4 inittask

In current Go, the linker emits a runtime.inittask for each package:

type inittask struct {
    state uint32           // 0=uninitialized, 1=in progress, 2=done
    nfns  uint32
    // followed by `nfns` function pointers to the package's init bodies
}

The runtime's doInit walks an array of *inittask (one per package, in dependency-respecting order) and invokes each.


3. Variable Initialization Order

3.1 The Algorithm

For each package, the compiler: 1. Builds a graph: nodes are package-level variable declarations and init blocks. 2. Edges: a node N depends on node M if N's initializer references something defined or computed by M. 3. Performs a topological sort. 4. Among nodes with no dependencies on each other, orders by source position (file alphabetical, then line number).

3.2 Worked Example

// a.go
var x = compute()
var y = x + 1
func compute() int { return 42 }

// b.go
var z = y * 2

Dependency graph: - yx - zy

Topological sort: x, y, z. Result: - x = compute() → 42 - y = x + 1 → 43 - z = y * 2 → 86

Note compute itself is a function, not a variable; it's available at any time. Only variable initialization order is sequenced.

3.3 Cycles

If you write:

var x = y + 1
var y = x + 1

The compiler emits: initialization cycle: x refers to y. Detection is purely static. The same kind of cycle inside init bodies is not detected statically (since arbitrary code might or might not run depending on flow), but at runtime would produce stack overflow or order-dependent results.

3.4 Across Files

The spec guarantees deterministic order across files of one package: by the order the files are presented to the compiler, traditionally alphabetical. The compiler now enforces this by sorting input files before doing the dependency analysis, then breaking ties by source position.


4. Runtime: doInit

4.1 Source

// src/runtime/proc.go (simplified)
func doInit(ts []*initTask) {
    for _, t := range ts {
        doInit1(t)
    }
}

func doInit1(t *initTask) {
    switch t.state {
    case 2:
        return
    case 1:
        throw("recursive call during initialization - linker skew")
    }
    t.state = 1
    // call each init function pointer in this package
    for i := uint32(0); i < t.nfns; i++ {
        f := *(*func())(addOffset(unsafe.Pointer(t), 8 + uintptr(i)*goarch.PtrSize))
        f()
    }
    t.state = 2
}

The initTask struct is laid out by the linker; f() may be the package's user init.0, init.1, or the synthesized package-var initializer.

4.2 firstmoduledata

The linker emits a runtime.firstmoduledata symbol describing the binary: types, function tables, GC bitmaps, and a slice of *initTask for all packages in the binary, in deepest-first dependency order.

The runtime startup (in runtime.main) does:

doInit(runtime_inittasks)  // runtime's own
doInit(activeModules()[0].inittasks) // user code's, deepest first
main_main()

So even before a single line of your main runs, the runtime has already executed every init in topological order.

4.3 Why Not Parallel

Runtime init is single-threaded. The runtime does not start the scheduler's worker goroutines until init completes. runtime.GOMAXPROCS is honored, but P0 is the only running P during init. This is essential for determinism: if init ran in parallel, you would have to add explicit happens-before relationships between packages, defeating the simple model.

You can spawn goroutines from within init (and they will run), but this is a notorious anti-pattern (see find-bug.md). The new goroutines are scheduled normally, but the initializing goroutine continues and main has not yet started.


5. The init Dependency Graph

5.1 Full Graph

Conceptually, three kinds of edges exist: 1. Package import edges: A imports B → B's init runs first. 2. Package-var dependency edges: var Y references var X (in same package) → X initialized first. 3. Source-order edges: ties broken by file alphabetical, then line.

5.2 Diamond Dependencies

main → A → C
main → B → C

Order: C, A, B, main (or C, B, A, main — the spec only requires C first; the toolchain picks one deterministically based on import order in source).

5.3 init Functions Are Black Boxes to the Graph

The compiler does not analyze what your init function does. Even if your init reads pkg.X, the dependency engine does not see that and reorder. The only guarantee is: B (imported by A) runs entirely before A.

So if you write:

// pkg a
import "pkg/b"
var Cache = b.LoadCache()  // analyzed, topo-sorted
func init() {
    Cache = augment(Cache, b.OtherTable) // not analyzed!
}
You depend on b.OtherTable already being computed. Since vars init before init, and B's vars init before A's, this works. But brittle.


6. init in the Test Binary

6.1 _test Packages

When go test builds a test binary for package foo, it produces a single binary linking: - foo (the package under test) - foo_test (the external test package, if any) - Any imports that either bring in - testing and testing/iotest etc.

All of these have their init run. So if foo imports database/sql and _ "github.com/lib/pq", every test invocation registers the postgres driver — even unit tests that never touch a DB.

6.2 TestMain

TestMain runs after all init functions but before any tests. It is the right place to do test-specific setup, fixture creation, and resource cleanup — work that should be parameterized rather than baked into init.

func TestMain(m *testing.M) {
    setup()
    code := m.Run()
    teardown()
    os.Exit(code)
}

setup is callable, mockable, and parameter-driven; init is none of those.


7. init Across Plugins (-buildmode=plugin)

When you build a plugin (go build -buildmode=plugin ...) and load it from a host process via the plugin package, the plugin's package inits are run at load time, after the host has already started.

Implications: - The plugin must not assume a particular phase of the host's lifecycle. - The plugin's init can register itself with host singletons that already exist — common pattern for extensibility. - A panic in plugin init does not crash the host; plugin.Open returns an error.

This is the only way to defer init after main starts. The runtime supports it via (*Plugin).init and the same inittask machinery.


8. Subtleties

8.1 init Can Reference Non-Package-Level Symbols

type T struct{ X int }
var t T
func init() { t.X = 42 }
Trivial, but a reminder: any symbol visible to the package is available in init.

8.2 init Cannot Be Inlined

The compiler does not inline init bodies. They are addressable indirectly via the inittask table.

8.3 init and CGO

If your package uses cgo, the cgo-generated init steps run as part of the package's init pipeline. Specifically, _cgo_init and any __attribute__((constructor)) C functions run before Go init returns. Mixing with goroutines started in init can cause subtle deadlocks.

8.4 init Receivers Don't Exist

A func (T) init() method is NOT special. The runtime sees only top-level func init() declarations. There is no concept of "type init" in Go.

8.5 init in main

main's init is just like any other package's. It runs after all imports' inits. main() runs after main's init finishes.

8.6 init and panic

init panics propagate to runtime.main, which prints the panic and exits with status 2. There is no opportunity to recover outside of within the init call stack itself. A common pattern:

func init() {
    defer func() {
        if r := recover(); r != nil {
            log.Fatalf("init failed: %v", r)
        }
    }()
    risky()
}
This converts a panic to a log.Fatalf (also exits, but with a cleaner message and known logger destination).


9. Reading the Spec Carefully

The relevant spec (https://go.dev/ref/spec#Package_initialization) has subtle clauses: - "Multiple such functions may be defined per package, even within a single source file." — yes, you can have many. - "The init identifier is not declared in the scope of the program." — that's why you can't call init(). - "The declarations may be presented in any order." — but they are evaluated in dependency order. - "If multiple packages are imported, they are initialized in dependency order." — depth-first, not breadth-first.

The careful reader notices the spec does not mandate alphabetical filename order; only deterministic order. Older Go versions (pre-1.5) used the order files appeared on the command line, which was usually alphabetical because of how the build system invoked the compiler — but technically, code that depends on filename order is non-portable.


10. Failure Modes the Senior Developer Must Recognize

10.1 Mysterious "registered driver" missing

A blank import was deleted (perhaps by goimports cleanup). Diagnostic: search the binary's symbols for the driver's init:

go tool nm ./bin | grep 'pq\.init'
If empty, pq is not linked.

10.2 init Order Surprise on Refactor

You move a file b.go to aa.go for cosmetic reasons. Now its inits run earlier; another package's init that read state assumed this ordering. Test passes locally, fails on gccgo. Lesson: don't depend on filename order.

10.3 init Goroutine Race

init starts a goroutine that reads package vars; main starts before the goroutine has finished. Diagnose with -race and structured logging of init phase.

10.4 Init-time Panic Becomes Production Outage

Because init panics happen before main, no graceful-shutdown machinery is available. The container restart loops if Kubernetes is configured aggressively. Lesson: validate inputs in main, not init.


11. Senior-Level Mental Checklist

  • Does this init only do constant-time, deterministic, no-I/O work?
  • Does it register with a registry rather than mutate global mutable state?
  • Will it run cleanly in go test with no env vars set?
  • If it can fail, is the failure clearly a programmer error, not a runtime/operator issue?
  • Have I avoided spawning goroutines?
  • Have I avoided cross-file init dependencies in this package?
  • If it's in a library, does it avoid mutating other packages' globals (e.g., http.DefaultServeMux)?
  • Is the corresponding blank import in callers commented?

You now know what init actually compiles and runs to. The professional level is about how teams operationalize this knowledge.