What Is the init Function and Package Initialization Order

The `init` function is a special function in Go that runs automatically before `main()` to set up package state, and it executes in a specific order: imports first (depth-first), then the package's own `init` functions.

The backstage crew of your program

You write a Go web server. You import a database driver. You expect to connect in main, but the driver needs to register itself before you can use it. Or you have a global configuration that must be loaded before any handler runs. Go solves this with init.

The init function runs automatically before main. It sets up package state, registers drivers, and validates environment variables. You cannot call init manually. You cannot pass arguments to it. You cannot return values from it. It exists solely for side effects that must happen before your program starts.

Think of init as the stage crew before the curtain rises. The actors in main never see the crew, but the lights are on, the props are placed, and the sound system is tested. If the crew fails to set up a prop, the show crashes before the first line is spoken. init is that setup phase. It runs silently, and if it panics, the program terminates immediately.

How init works

Every package can define init functions. The compiler collects them and ensures they run before main. The signature is fixed: no name, no arguments, no return values.

Here is the simplest possible init function:

package main

import "fmt"

func init() {
	// Runs automatically before main, prints a message to stdout
	fmt.Println("init runs first")
}

func main() {
	// main runs after all init functions in the import graph
	fmt.Println("main runs second")
}

The compiler sees init and treats it specially. At runtime, the Go runtime executes all init functions in the correct order, then jumps to main. You never write a call to init. If you try to call it, the compiler rejects the code with init is not a function. The identifier init is reserved for this purpose. You cannot define a variable named init either; the compiler complains with init is not a variable.

A package can have multiple init functions. This is allowed, though rare. The compiler runs them in the order they appear in the source files. If a package has files a.go and b.go, the init functions in a.go run before those in b.go. The order is lexicographical by file name. This means config.go runs before server.go. Relying on file name order is fragile. Rename a file and the order changes. Keep init functions independent of each other whenever possible.

Init functions run in file order. Name your files carefully if you have multiple inits.

Initialization order

Go initializes packages in a strict depth-first traversal of the import graph. When a package is imported, Go initializes all its dependencies first. This guarantees that by the time a package's init runs, everything it imports is already ready.

Consider a chain of imports. Package main imports utils. Package utils imports config. The order is config, then utils, then main.

Here is a minimal example showing the import order:

// package config
package config

import "fmt"

func init() {
	// Runs first because config has no imports
	fmt.Println("config init")
}
// package utils
package utils

import (
	"fmt"
	"myapp/config" // Import triggers config.init before utils.init
)

func init() {
	// Runs second, after config is fully initialized
	fmt.Println("utils init")
}
// package main
package main

import (
	"fmt"
	"myapp/utils" // Import triggers utils.init before main.init
)

func init() {
	// Runs third, after all dependencies are ready
	fmt.Println("main init")
}

func main() {
	fmt.Println("main function")
}

The output is deterministic:

# output:
config init
utils init
main init
main function

The runtime builds the import graph, sorts it depth-first, and executes init functions in that order. Circular imports are impossible in Go. The compiler detects cycles and rejects the program with import cycle not allowed. This prevents infinite loops during initialization.

Within a single package, variable initializations and init functions run in declaration order. If a variable is declared after an init function, the init runs first. This interleaving can cause subtle bugs if an init function reads a variable that hasn't been initialized yet. The compiler does not catch this. It is a runtime logic error.

Trust the import graph. Depth-first order guarantees dependencies are ready. Don't rely on file names for logic.

Real-world usage

The most common use of init is registering drivers or plugins. The database/sql package uses this pattern. Drivers register themselves by name in their init functions. When you import a driver, its init runs and registers the driver. Later, sql.Open can find the driver by name.

Here is how a driver package registers itself:

package mydriver

import "database/sql"

var driverName = "mydriver"

func init() {
	// Register makes the driver available by name so sql.Open can find it
	sql.Register(driverName, &myDriver{})
}

type myDriver struct{}

func (d *myDriver) Open(name string) (driver.Conn, error) {
	// Open implements the driver interface
	return nil, nil
}

The main package imports the driver. The import triggers init, which registers the driver. No explicit call is needed.

package main

import (
	_ "myapp/mydriver" // Blank import triggers init, registering the driver
)

func main() {
	// sql.Open works because init registered the driver
	db, err := sql.Open("mydriver", "dsn")
	// handle err
}

The blank import _ tells the compiler to import the package for its side effects only. The side effect is the init function. This is the standard pattern for drivers, codecs, and plugins.

Another valid use is loading configuration that has no dependencies on command-line arguments. If your config comes from a fixed file path or environment variables, init can load it into a global variable.

package config

var GlobalConfig Config

func init() {
	// Load config from env vars, panic if missing
	data, err := loadFromEnv()
	if err != nil {
		panic(err)
	}
	GlobalConfig = data
}

Panic in init is acceptable for fatal startup errors. If the config is missing, the program cannot run. Panicking stops execution immediately. This is better than ignoring the error and failing later with a cryptic message.

Init is for registration and fatal setup. If you can return an error, use a constructor instead.

Pitfalls and errors

init functions have limitations. You cannot return errors. You cannot accept parameters. You cannot test them easily. These constraints make init unsuitable for complex logic.

If you try to return an error from init, the compiler rejects it with init cannot have return values. If you try to add parameters, you get init cannot have parameters. The signature is fixed.

Complex logic in init is hard to debug. If init panics, the stack trace points to the panic, but the cause might be buried in a dependency. If init fails silently, the program runs with broken state. Both outcomes are worse than a clear error in main.

The community convention is to avoid init for anything beyond registration and simple global setup. If you need to initialize state based on arguments, use a constructor function. Constructors accept parameters and return errors. They are testable and explicit.

Here is the contrast between bad and good initialization:

// Bad: Complex logic in init, cannot return error
func init() {
	// Load config, panic on error
	cfg, err := loadConfig()
	if err != nil {
		panic(err)
	}
	// Set global state
	GlobalConfig = cfg
}

// Good: Explicit constructor, returns error
func NewConfig() (*Config, error) {
	// Load config and return error to caller
	return loadConfig()
}

The constructor approach lets main handle the error. main can print a helpful message and exit cleanly. init forces you to panic or ignore errors. Ignoring errors in init is dangerous. The program runs with invalid state.

Another pitfall is order dependency. If package A's init depends on package B's init, you must import B in A. This creates a hard dependency. If the dependency is implicit, the code breaks when imports change. Make dependencies explicit. Import what you use.

Init cannot return errors. If you need to report failure, use a constructor. Panic only for fatal startup conditions.

Variable initialization vs init

Go allows variable initialization with function calls. This runs during the initialization phase, just like init.

package main

var x = computeX()

func computeX() int {
	return 42
}

func init() {
	// init runs after variable declarations in declaration order
}

Variable initializations and init functions are interleaved based on declaration order. If computeX is declared after init, init runs first. This can cause x to be zero when init reads it. The compiler does not check this. It is a runtime bug.

To avoid this, keep variable initializations simple. Use init for logic that depends on other packages. Use variable declarations for local computations. If the order matters, use init explicitly.

Variable inits run in declaration order. Don't mix complex logic with variable declarations.

When to use init

Choose the right tool for initialization. init is powerful but limited. Constructors are flexible and testable. main is for program entry.

Use init when you need to register a driver or plugin that must be available before main runs. Use init when you are loading a configuration file that has no dependencies on command-line arguments and failure is fatal. Use a constructor function like New() when you need to return an error or accept parameters. Use main for initialization that depends on CLI flags or user input. Use a variable declaration with a function call when the initialization is a simple expression with no side effects.

Init is for setup, not logic. Keep it boring. Constructors are for everything else.

Where to go next