How to Use the init() Function in Go

The init function is a special Go function that runs automatically before main to initialize package state.

The setup that happens before the show starts

You are building a database driver. You want users to import your package and have the driver immediately available to the database/sql package. You do not want users to call a RegisterDriver() function manually. You want the act of importing your package to perform the registration.

Go provides a mechanism for this. It is called init. The init function runs automatically before main. You never call it. The compiler calls it. It has no arguments and no return value. It exists to set up package state, register drivers, or validate configuration before any other code in the package executes.

Think of init as the stage crew. Before the actors step on stage, the crew sets the lights, checks the props, and locks the doors. If the crew forgets a prop, the show does not start. init runs once per package. It runs in a deterministic order. It is the only place in Go where code executes automatically without an explicit call.

How init works

An init function looks like a regular function, but the compiler treats it as a special keyword. The signature is fixed. You define it in any .go file within a package. The compiler collects all init functions in the package and generates code to call them before main starts.

Here is the skeleton of an init function. It runs setup code automatically.

package main

import "fmt"

func init() {
	// init has no parameters and no return value.
	// The compiler calls this automatically before main.
	fmt.Println("Package initialized")
}

func main() {
	// main runs after all init functions in the program.
	fmt.Println("Hello")
}

The compiler enforces strict rules on init. You cannot add parameters. You cannot add return values. You cannot call init from your code. If you try to break these rules, the compiler rejects the program.

Attempting to add arguments triggers init cannot have parameters. Adding a return type triggers init cannot have results. Trying to call it manually triggers cannot call init. These errors are hard stops. The function name init is reserved for this automatic behavior.

You can have multiple init functions in a single package. This is common when a package has a large initialization routine that you want to split across files. The compiler calls all of them. The order within a file is the order of definition. The order across files is deterministic based on the file names.

gofmt formats init functions like any other function. There is no special indentation rule. Trust the formatter. If you split init logic across files, gofmt keeps the code consistent.

Init is automatic. Don't make it manual.

The order of execution

Initialization order matters when packages depend on each other. Go resolves the import graph before running any code. If package A imports package B, all of B's init functions run before any of A's init functions. This guarantees that dependencies are ready before the dependent package tries to use them.

Here is a realistic example showing how init order flows through imports. The driver registers itself, and the main package uses it.

// driver.go
package mydriver

import "database/sql"

// Driver implements the sql.Driver interface.
type Driver struct{}

func init() {
	// Register the driver in the global sql registry.
	// This happens when the package is imported.
	// sql.Register stores the driver by name.
	sql.Register("mydriver", &Driver{})
}
// main.go
package main

import (
	// Blank import runs init but discards the package name.
	// This triggers the registration side effect without using the package.
	_ "mydriver"

	"database/sql"
)

func main() {
	// The driver is registered because mydriver's init ran.
	// Open uses the name "mydriver" to find the driver.
	db, err := sql.Open("mydriver", "dsn")
	if err != nil {
		panic(err)
	}
	_ = db
}

The blank import _ "mydriver" is a convention. The underscore discards the package name, signaling that you only care about the side effects of the import. The side effect is the init function. Without the blank import, the compiler warns about an unused import. With the blank import, the compiler knows you want the init to run.

The order is:

  1. Imports are resolved recursively.
  2. init functions run in dependency order.
  3. main runs last.

If package A imports B and C, and B imports C, the order is C, then B, then A. This prevents circular initialization issues. Go detects circular imports at compile time and rejects them with import cycle not allowed. You cannot have circular dependencies. The import graph must be a directed acyclic graph.

Init runs once. If you import a package multiple times, its init runs only once. The compiler deduplicates imports.

Order is deterministic. Test it.

Pitfalls and errors

init is powerful, but it introduces hidden dependencies. Code in init runs before you can control it. If init fails, the program may panic or continue in a broken state. There is no way to return an error from init to main. If you need to report an error, you must panic.

Panic in init terminates the program. There is no recovery. If your init function panics, the program crashes with a stack trace. This is useful for fatal setup errors, like missing configuration files or invalid environment variables. It is not useful for recoverable errors.

Here is an example of handling errors in init. The pattern is to panic on failure.

package config

import (
	"os"
	"path/filepath"
)

var ConfigPath string

func init() {
	// Load config path from environment.
	// If the variable is missing, panic because the program cannot run.
	path := os.Getenv("APP_CONFIG")
	if path == "" {
		panic("APP_CONFIG environment variable is required")
	}
	// Resolve the absolute path.
	abs, err := filepath.Abs(path)
	if err != nil {
		panic(err)
	}
	ConfigPath = abs
}

The if err != nil pattern applies inside init. If you get an error, you panic. You cannot return the error. The community accepts this boilerplate because it makes the failure mode explicit. A panic in init is a loud signal that setup failed.

Testing code that uses init can be difficult. init runs every time you run tests. If init modifies global state, that state persists across tests. You cannot reset init between tests. This makes unit tests fragile.

To mitigate this, keep init small. Use init for registration and global state setup. Move business logic to exported functions. Use constructor functions like New() to create instances with configurable state. This allows tests to create fresh instances without relying on init.

Another pitfall is using init to hide dependencies. If a function relies on global state set up in init, that dependency is invisible. The function signature does not show what it needs. This makes code harder to understand and refactor. Explicit dependencies are better. Pass configuration as parameters. Return errors. Avoid global state when possible.

init cannot be called. You cannot test init directly. You test the effects of init. If init registers a driver, you test that the driver is available. If init sets a global variable, you test that the variable has the expected value.

The worst init bug is the one that silently corrupts state. Panic early.

When to use init

init has specific use cases. It is not a general-purpose constructor. Use it for package-level setup that must happen automatically. Avoid it for logic that should be explicit.

Use init when you need to register a driver or plugin via a blank import. This is the most common and accepted use case. It allows packages to contribute to a global registry without requiring manual calls.

Use init when you must initialize global state that other packages depend on at startup. Examples include loading configuration from environment variables or setting up logging. The state must be ready before any other code runs.

Use a constructor function like New() when you need to return an error or create multiple instances. Constructors allow callers to handle errors and configure the object. init cannot return errors and runs only once.

Use main for setup when the initialization is specific to the application binary and not shared library logic. main is the entry point for the application. It can handle command-line flags, parse arguments, and orchestrate startup. init is for packages. main is for the binary.

Avoid init when you can pass dependencies explicitly. Hidden setup makes code harder to test and reason about. If a function needs a database connection, pass the connection. Do not rely on init to set a global variable. Explicit dependencies improve readability and testability.

Use init for side effects. Use functions for logic.

Where to go next