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.

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:
package a
import "fmt"
var Value = initValue()
func initValue() int {
fmt.Println("a: package variable initialized")
return 1
}
func init() {
fmt.Println("a: init")
}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" }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)
}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 0The 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:
package main
import "fmt"
var total = compute()
var base = 10
func compute() int {
return base * 2
}
func main() {
fmt.Println(total, base)
}20 10total 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:
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]() }package alpha
import "example.test/lab/plugin"
func init() {
plugin.Register("alpha", func() string { return "alpha ran" })
}package beta
import "example.test/lab/plugin"
func init() {
plugin.Register("beta", func() string { return "beta ran" })
}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"))
}[alpha beta]
beta ranmain 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 _ = ....
Related rules worth knowing
You cannot call init() yourself, and you cannot declare anything else with that name:
package main
func init() {}
func main() {
init()
}# example.test/lab
./main.go:6:2: undefined: initThe 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.


