Skip to content
gohacks.dev
Basics

The init Function in Go and the Cases Where It Hurts

init runs once per package before main, after variables are set up and dependencies are initialized. See the exact order, driver registration and its costs.

6 min read
Summarize in AI
Flat illustration of an editor window full of code.

The init function is the one place in Go where code runs without anything calling it. That is useful for exactly one pattern and a liability for most others, and the line between them is easy to cross because init is so easy to write. This article states the precise order of initialization, shows it running across three packages, explains the registration pattern behind database/sql drivers, and lists what goes wrong when init does more than register. It belongs in the Go basics path, builds on packages and exported names, and its zero-value counterpart is designing types around a useful zero value.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

The order, exactly

Go initializes a program in a fixed sequence, defined in the specification's section on package initialization. For each package, in dependency order: first all the imported packages, then the package-level variables, then the init functions. Here are three packages that print as they go, so the order is visible:

a/a.goGo
package a

import "fmt"

var Value = initValue()

func initValue() int {
	fmt.Println("a: package variable initialized")
	return 1
}

func init() {
	fmt.Println("a: init")
}
b/b.goGo
package b

import (
	"fmt"

	"example.test/lab/a"
)

func init() {
	fmt.Println("b: init, a.Value is", a.Value)
}

func Hello() string { return "b says hello" }
main.goGo
package main

import (
	"fmt"

	"example.test/lab/b"
)

var mainVar = func() int {
	fmt.Println("main: package variable initialized")
	return 0
}()

func init() {
	fmt.Println("main: first init")
}

func init() {
	fmt.Println("main: second init")
}

func main() {
	fmt.Println("main:", b.Hello(), mainVar)
}
Text
a: package variable initialized
a: init
b: init, a.Value is 1
main: package variable initialized
main: first init
main: second init
main: b says hello 0

The output reads top to bottom as the program starts. main imports b, which imports a, so a goes first: its variable, then its init. Then b's init, which can safely use a.Value because a has finished. Then main's own variable, its two init functions in source order, and finally main itself. Two details are worth remembering. A package is initialized only once, however many packages import it. And main never imports a directly, yet a ran, because initialization follows the dependency graph and not the import statements in one file.

When a package has several files, the compiler receives them in the order the go command passes them, which is sorted by file name. The spec encourages build systems to present files in sorted order, and nothing guarantees it, so code that depends on init order between two files of one package is fragile.

Variables initialize by dependency, not by line

Package-level variables are not set in the order they are written. A variable is initialized once everything it refers to is ready, so a variable declared first can be set last:

main.goGo
package main

import "fmt"

var total = compute()

var base = 10

func compute() int {
	return base * 2
}

func main() {
	fmt.Println(total, base)
}
Text
20 10

total depends on compute, which refers to base, so base is initialized first and the program prints 20 10. In a function body that order would be a bug, and at package level the compiler resolves it. This is handy and it can hide a cycle: if base also depended on total, the compiler stops with initialization cycle. The rule does not extend to init functions, which run strictly after all variables.

The legitimate use: registration

The standard library's drivers and codecs register themselves in init. The importing program never calls anything in the driver package. It imports the package with the blank identifier, which runs its init and discards the names:

plugin/plugin.goGo
package plugin

import "sort"

var registry = map[string]func() string{}

func Register(name string, f func() string) {
	registry[name] = f
}

func Names() []string {
	names := make([]string, 0, len(registry))
	for n := range registry {
		names = append(names, n)
	}
	sort.Strings(names)
	return names
}

func Run(name string) string { return registry[name]() }
alpha/alpha.goGo
package alpha

import "example.test/lab/plugin"

func init() {
	plugin.Register("alpha", func() string { return "alpha ran" })
}
beta/beta.goGo
package beta

import "example.test/lab/plugin"

func init() {
	plugin.Register("beta", func() string { return "beta ran" })
}
main.goGo
package main

import (
	"fmt"

	_ "example.test/lab/alpha"
	_ "example.test/lab/beta"
	"example.test/lab/plugin"
)

func main() {
	fmt.Println(plugin.Names())
	fmt.Println(plugin.Run("beta"))
}
Text
[alpha beta]
beta ran

main names neither plugin, and both are registered. This is how database/sql works, where import _ "github.com/jackc/pgx/v5/stdlib" makes sql.Open("pgx", ...) work, and how image/png registers a decoder so that image.Decode can read PNG files. The pattern keeps the core package free of knowledge about its extensions. Its cost is that the link between the import and the behavior is invisible: delete an import that looks unused and a feature disappears.

Where init causes trouble

Hidden failures. An init cannot return an error, so a failure becomes a panic or a silent default. A package that connects to a database in init crashes every program that imports it, including tests that do not need a database.

Hard to test. init runs before any test can set things up, so it cannot be told to use a fake. Configuration read from the environment in init is fixed before TestMain runs.

Order coupling. Code that works because one init ran before another is one rename away from breaking, since the order across files is a tooling detail.

Global state. A package-level variable set in init is shared by everyone and changes under tests that run in parallel. A registry is acceptable because it is written once at startup and read afterwards, and a mutable setting is not.

Startup cost. Everything in every imported package's init runs on every start of the program, including the --help of a command line tool. A heavy init such as compiling a large table or loading a file slows each launch for all importers. A lazily initialized value with sync.Once pays only when used, as the sync Once family shows.

The alternative to init is almost always an explicit function. A package exports func New(cfg Config) (*Client, error), the program calls it from main, and the error goes where errors go. Wiring happens in one place and a test can build its own client. I allow init for three things: registering with a registry, setting up a table that is a pure computation and cannot fail, and compile-time-style assertions written as var _ = ....

You cannot call init() yourself, and you cannot declare anything else with that name:

main.goGo
package main

func init() {}

func main() {
	init()
}
Text
# example.test/lab
./main.go:6:2: undefined: init

The compiler answers undefined: init, because the name is not in scope as something you can use, and declaring var init = 1 is refused as well with cannot declare init - must be func. A func main is similarly special, runs after all of this and ends the program when it returns. Goroutines started in init are legal, and they run concurrently with the rest of initialization, which is almost always a bug.

When init is fine and when to refuse it

If you maintain a library, resist the urge to put work in init, since you are imposing it on every importer, including those who import a single constant. If you write an application, a few init functions in main are no worse than the same code at the top of main, though the explicit form is easier to read. And when you review code, look hard at any init that reads a file, opens a connection, parses flags or logs: those are the ones that turn "import this package" into a side effect. The Effective Go section on init describes the intended uses, and the Go blog's package names article touches on where registration belongs.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.