---
title: The init() Function Execution Order Gotcha
summary: Enforce init() execution order by calling setup functions sequentially from a single init() function.
lang: en
slug: the-init-function-execution-order-gotcha
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.gofaq.org/en/the-init-function-execution-order-gotcha/
---

# The init() Function Execution Order Gotcha



## The hidden queue

You split your package into three files to keep things tidy. One file reads environment variables. Another validates them against a schema. The third opens a database connection. You drop an `init()` function in each file, assuming they will run in the order you wrote them. The program starts. The database connection fails because the validation step never executed. You stare at the terminal, wondering why Go decided to run the files in a different sequence than you expected.

This is not a bug. This is how Go handles package initialization. The language guarantees that `init()` functions run before `main()`, but it deliberately leaves the cross-file order undefined. File names are labels, not a queue.

## How Go actually orders init

Go treats a package as a single compilation unit, not a collection of ordered scripts. When the compiler processes a package, it gathers every `init()` function from every `.go` file in that directory. It does not sort them alphabetically. It does not follow import order. It chains them together based on internal file hashing and build parallelism. This design exists because Go prioritizes fast, deterministic compilation across large codebases. Requiring strict file-level ordering would force the build system to serialize file processing, slowing down compilation and complicating tooling.

Inside a single file, the rules are simple. Multiple `init()` functions run from top to bottom. The compiler preserves source order within that file. Across files, the compiler shuffles them during the build step. At runtime, the Go runtime executes the entire initialization chain in a single goroutine before handing control to `main()`. No concurrency exists yet. No HTTP servers are listening. No worker pools are running. The process is purely sequential setup.

File names are labels, not a queue.

## The minimal proof

Here is the simplest way to observe the unpredictability. Create two files in the same package.

```go
// a.go
package main

func init() {
    // prints first in some builds, second in others
    println("A initialized")
}
```

```go
// b.go
package main

func init() {
    // order relative to a.go is undefined by the language spec
    println("B initialized")
}
```

```go
// main.go
package main

func main() {
    // runs only after all init functions complete
    println("main started")
}
```

When you run this program, you will see either `A initialized` then `B initialized`, or the reverse. The compiler collects both `init()` functions, shuffles them during the build step, and links them into a single initialization chain. At runtime, the Go runtime executes that chain before calling `main()`. You cannot rely on alphabetical sorting or file creation time to dictate sequence. The language specification explicitly states that the order is implementation-defined and may change between compiler versions.

The compiler chains them. You cannot predict the shuffle.

## Real package setup

Real projects need deterministic setup. A configuration package often reads flags, validates them, and initializes external clients. Splitting this across files without explicit ordering creates hidden dependencies. The fix is to consolidate the entry point. You keep your code organized across files, but you funnel the execution sequence through a single `init()` function that calls named setup functions in the exact order you want.

```go
// config.go
package config

var dbURL string

func init() {
    // explicit ordering guarantees env vars load before validation
    loadEnvVars()
    validateConfig()
    connectDatabase()
}

func loadEnvVars() {
    // reads from environment or falls back to defaults
    // keeps the actual parsing logic isolated for testing
}

func validateConfig() {
    // checks required fields and formats
    // panics if configuration is invalid, crashing early
}

func connectDatabase() {
    // opens the connection using the validated URL
    // stores the client in a package-level variable
}
```

The compiler still sees only one `init()` in this file. It runs it. Inside, you explicitly call the setup functions in the exact order you want. The compiler cannot reorder function calls inside a function body. You trade a few lines of boilerplate for guaranteed execution sequence. The other files in the package can still hold helper functions, type definitions, or additional logic. They just do not define their own `init()` functions. If you need to split initialization across files for organizational reasons, you can still use multiple `init()` functions in one file, and they will run top to bottom. But cross-file ordering remains undefined.

Explicit calls beat implicit ordering every time.

## Where init goes wrong

`init()` functions run in a single goroutine before any concurrency exists. If you panic inside `init()`, the entire program crashes before `main()` ever executes. You will see a stack trace that points directly to your initialization code, often with a message like `panic: runtime error: index out of range` or `panic: assignment to entry in nil map`. The compiler will not warn you about logical ordering mistakes. It only catches syntax and type errors. If you forget to call a setup function, the compiler happily builds the binary. The bug surfaces at runtime as missing state or failed connections.

Another trap is circular package initialization. If package A imports package B, and package B imports package A, the compiler rejects it with `import cycle not allowed`. Even without cycles, heavy `init()` chains can hide dependencies that make testing nearly impossible. Unit tests expect isolated, predictable state. When initialization happens automatically at package load time, you cannot easily reset state between tests. You end up writing integration tests instead of unit tests, or you resort to global flags to skip initialization during test runs.

The Go community generally discourages heavy `init()` usage. The convention is to keep `init()` short, or avoid it entirely in favor of explicit constructors. A function like `NewConfig()` that takes parameters and returns a fully initialized struct is easier to test, easier to read, and impossible to run in the wrong order. The `init()` function takes no parameters and returns nothing. It is a hook, not a design pattern. When you rely on it for complex setup, you are trading clarity for convenience.

A panic in init is a crash before the game starts.

## When to reach for init

Initialization is setup, not magic. Choose the right tool based on your dependency graph and testing needs.

Use a single `init()` with explicit function calls when you need deterministic ordering within a package.

Use package-level variables initialized by explicit constructors when you want to test setup logic in isolation.

Use `main()` for complex orchestration when initialization depends on command-line flags or user input.

Avoid `init()` entirely when you can pass dependencies explicitly through function parameters and struct constructors.

Initialization is setup, not magic.

## Where to go next

- [How to check if value implements interface](/en/how-to-check-if-value-implements-interface/)
- [How to limit concurrent goroutines](/en/how-to-limit-concurrent-goroutines/)
- [How Popular Is Go: TIOBE, Stack Overflow, and GitHub Trends](/en/how-popular-is-go-tiobe-stack-overflow-and-github-trends/)