init Functions
Understand Go init functions, their execution order, and how to use them for automatic package setup
An init function is a special function in Go that the runtime calls automatically before your program starts executing the main function. You never invoke init yourself, and it takes no arguments and returns nothing. Its purpose is to let you perform one‑time initialization for a package: setting up global state, validating configuration, registering drivers, or performing any task that must happen exactly once, at startup.
init is a function, not a keyword:
The name init is not a reserved keyword. It is a convention the compiler recognises: any function declared as func init() { ... } at the package level (top‑level, not inside another function) will be treated as an initialisation function. You cannot call it from your code.
The Problem init Solves
When a Go program starts, every imported package must be ready before the main function begins. Some packages need to build lookup tables, parse configuration, or register themselves with a central registry. Without a standard initialisation hook, each package would need an explicit Initialize() function that every user must remember to call—fragile and error‑prone.
init solves this by providing a guarantee: all init functions in all imported packages run before main, in a well‑defined order. The programmer writing the package decides what needs to be initialised; the programmer importing the package gets that initialisation automatically, with no extra steps.
No action needed from the importer:
If you import a package that contains init functions, they will run. You do not need to invoke anything. The Go runtime handles this for you.
The Order of Initialisation
The execution order of init functions is strictly defined by the language specification, and understanding this order is essential to avoid subtle bugs. Go initialises packages in a bottom‑up, dependency‑first sequence.
For a single package, the order is:
- Imported packages are initialised first, recursively.
- Package‑level constants are evaluated (they are known at compile time, but any dynamic initialisation of constants would happen here; in practice, constants are resolved during compilation).
- Package‑level variables are initialised in declaration order, respecting any dependencies between variables. If a variable’s initialiser calls a function that refers to another variable in the same package, that other variable must be initialised first.
initfunctions run, in the order they appear in the source files (or in lexical file name order across multiple files).
For a program, the chain looks like this:
import pkgA → import pkgB → ... → main package variables → main.init() → main.main()
Each imported package goes through the same cycle. A package is initialised only once, even if it is imported by multiple other packages.
Do not rely on init order across separate packages:
While the dependency order is deterministic, the relative order of init functions in two packages that do not import each other is not guaranteed and may change with compiler versions. Your code must not depend on one package’s init running before another’s unless there is a direct import chain.
Here is a minimal example that illustrates the ordering within a single file:
package main
import "fmt"
var message string
func init() {
message = "initialized"
fmt.Println("first init")
}
func init() {
fmt.Println("second init")
}
func main() {
fmt.Println("main:", message)
}
When you run this program, the output is:
first init
second init
main: initialized
The two init functions execute in the order they appear, after message has been initialised to its zero value ("") and before main starts. The first init sets message to "initialized"; the second simply prints.
Variable Initialisers and init
Package‑level variables can be initialised with the result of a function call, and those calls happen before any init function in the same package. This can be used to enforce a specific sequence.
package main
import "fmt"
var answer = computeAnswer()
func computeAnswer() int {
fmt.Println("computing answer")
return 42
}
func init() {
fmt.Println("init running, answer is", answer)
}
func main() {
fmt.Println("main running, answer is", answer)
}
The output shows that computeAnswer runs first, then init, then main:
computing answer
init running, answer is 42
main running, answer is 42
This pattern is common when a package needs to build a complex data structure and then perform additional setup that builds on it. The variable initialiser builds the core structure; init can register the package or validate invariants.
Multiple init Functions in the Same File
Go allows you to declare init multiple times inside the same file. There is no practical limit, and each one runs in the order it appears in the source text.
package main
import "fmt"
func init() {
fmt.Println("Step 1: database connection pool configured")
}
func init() {
fmt.Println("Step 2: cache warmed")
}
func init() {
fmt.Println("Step 3: health checks passed")
}
func main() {
fmt.Println("Server starting")
}
When this runs, the steps appear in order. This is useful when a package’s initialisation is too large to keep in a single function and splitting it into smaller logical units improves readability. However, the convention is to keep init functions small; if you need many, consider whether the package is doing too much.
Avoid complex logic in init functions:
If an init function panics, the whole program crashes before main can recover. Any code that can fail should use explicit error handling, not init. See the section on alternatives later in this document.
Multiple init Functions Across Files in a Package
A package often spans multiple .go files in the same directory. The Go specification recommends that build systems present these files to the compiler in lexical file name order. This means a.go will be processed before b.go, and init functions from a.go will run before those from b.go.
Consider a package with this directory structure:
calculator/
a.go
b.go
a.go:
package calculator
import "fmt"
func init() {
fmt.Println("a.go init")
}
b.go:
package calculator
import "fmt"
func init() {
fmt.Println("b.go init")
}
When any program imports calculator, the output will be:
a.go init
b.go init
If you rename b.go to x.go, the order reverses because a.go < x.go lexically. This is fragile: a rename that seems harmless can change the execution order and break code that accidentally depended on it.
Order between files is fragile:
Do not assume that init functions in config.go will run before those in server.go just because of the file names. The lexical order depends on the exact names and the build system’s presentation. If two init functions in the same package must run in a specific order, put them in the same file, or let one depend on a variable set by the other via a function call in a variable initialiser.
Importing for Side Effects
One of the most visible uses of init in the standard library is the blank identifier import: importing a package solely so that its init functions run, even though you never reference any exported name from that package.
import _ "image/png"
This line registers the PNG image decoder with the image package. The image/png package contains an init function that calls image.RegisterFormat("png", pngHeader, Decode, DecodeConfig). After the import, image.Decode knows how to decode PNG images.
Without the import, your program would compile but would fail at runtime when trying to decode a PNG, because no decoder is registered.
The same pattern is used by database drivers:
import _ "github.com/lib/pq"
The pq driver registers itself with the database/sql package during init. Your code only ever uses the generic database/sql API and never mentions pq directly.
Blank imports are explicit about side effects:
The blank identifier _ tells the compiler: “I am importing this package purely for its side effects.” The intent is clear, and the compiler will not complain about an unused import.
When to Avoid init (And What to Use Instead)
Although init is convenient, it has significant drawbacks that make it unsuitable for many real‑world scenarios. Understanding these limitations will help you decide when to use it.
Drawbacks of init
- Implicit control flow: The initialisation happens globally, far from where the state is used. This makes code harder to read and debug.
- No error return: If an
initfunction encounters an error, it can only panic. A panic at startup may be acceptable for a simple CLI tool, but it is catastrophic in a long‑running server or library. - Testing difficulties: Global state set by
initmakes it difficult to isolate unit tests. You cannot easily reset the state between tests, and you cannot swap dependencies. - Tight coupling: Any package that imports a package with side‑effect‑rich
initbecomes coupled to that side effect, often unnecessarily.
The recommended alternative is explicit initialisation with constructors and dependency injection.
Instead of this:
package db
import "database/sql"
var Pool *sql.DB
func init() {
var err error
Pool, err = sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
}
Write this:
package db
import "database/sql"
type Database struct {
Pool *sql.DB
}
func New(connStr string) (*Database, error) {
pool, err := sql.Open("postgres", connStr)
if err != nil {
return nil, err
}
return &Database{Pool: pool}, nil
}
The caller can handle the error, pass the Database instance to components that need it, and replace it with a mock in tests.
Prefer explicit constructors:
When in doubt, use a constructor function. It gives you control over error handling, testing, and lifecycle. Reserve init for cases where you truly need automatic, package‑level setup that cannot be expressed with a function call (e.g., registering with a registry that expects to be populated at import time).
Common Mistakes
- Relying on init order across packages: Two packages that do not import each other have no guaranteed order. A program that works today may break after a refactor that changes import graphs.
- Expensive operations in init: Network calls, file reads, or large allocations in
initslow down every test and every binary that imports the package, even if the imported functionality is never used. - Panicking in init: A panic in an
initfunction leaves no room for graceful shutdown. In library code, this is especially harmful because it brings down the entire application that imports it. - Using init where explicit setup would suffice: If your package can expose a
Setup()orNew()function, prefer that. It keeps the package usable in contexts where automatic initialisation is not wanted.
init panics are unrecoverable:
Because init runs before main, there is no recover that can catch a panic from init and keep the program alive. The only way to handle errors is to avoid panicking in init entirely.
Summary
init functions give Go packages a way to run setup code automatically before main. They run after imported packages are initialised, after package‑level variables are set, and before main begins. You can have multiple init functions per file and across files in a package, but the execution order between files depends on lexical file names, making it fragile.
The most legitimate use of init is when a package needs to register itself with a global registry that must be populated at import time—database drivers and image decoders are canonical examples. For most other initialisation tasks, an explicit constructor function that returns an error is more robust, testable, and maintainable.