Skip to content

Init Functions & Package Initialization Intermediate

Introduction

Go has a special init() function that runs automatically when a package is loaded — before main() executes. Unlike regular functions, init() takes no arguments, returns nothing, and can appear multiple times in a single file or across files in a package. Package initialization follows a deterministic order: imported packages first (recursively), then package-level variables, then init() functions. Understanding this order, the blank import pattern, and when init() is appropriate (or not) is important for Go interviews and for debugging initialization issues.


Syntax & Usage

Basic init() Function

package main

import "fmt"

var config string

func init() {
    config = "production"
    fmt.Println("init: config set to", config)
}

func main() {
    fmt.Println("main: config is", config)
}

// Output:
// init: config set to production
// main: config is production

Multiple init() Per File

A single file can have multiple init() functions. They execute in the order they appear.

package main

import "fmt"

func init() {
    fmt.Println("first init")
}

func init() {
    fmt.Println("second init")
}

func init() {
    fmt.Println("third init")
}

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

// Output:
// first init
// second init
// third init
// main

Multiple init() Across Files

When a package has multiple files, init() functions execute in source file name order (alphabetical).

mypackage/
├── a_setup.go     // init() runs first
├── b_config.go    // init() runs second
└── c_validate.go  // init() runs third
// a_setup.go
package mypackage

func init() {
    fmt.Println("a_setup init")
}

// b_config.go
package mypackage

func init() {
    fmt.Println("b_config init")
}

// Output:
// a_setup init
// b_config init

Don't Rely on File Order

While Go guarantees alphabetical file order within a package, relying on cross-file init() ordering is fragile and makes code hard to understand. If initialization order matters, make it explicit through function calls.


Package Initialization Order

The complete initialization sequence is deterministic:

1. Imported packages are initialized first (recursively, depth-first)
2. Package-level variables are initialized (in declaration order, respecting dependencies)
3. init() functions run (in source file order, then declaration order within a file)
4. main() runs (only in the main package)
package main

import "fmt"

var a = func() int {
    fmt.Println("var a initialized")
    return 1
}()

var b = func() int {
    fmt.Println("var b initialized")
    return 2
}()

func init() {
    fmt.Println("init() runs")
}

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

// Output:
// var a initialized
// var b initialized
// init() runs
// main() runs

Full Dependency Chain

// pkg_c.go
package c

import "fmt"

func init() { fmt.Println("package c: init") }

// pkg_b.go
package b

import (
    "fmt"
    _ "example/c" // imports c
)

func init() { fmt.Println("package b: init") }

// main.go
package main

import (
    "fmt"
    _ "example/b" // imports b (which imports c)
)

func init() { fmt.Println("main: init") }

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

// Output:
// package c: init     (deepest dependency first)
// package b: init
// main: init
// main: main()

Each Package Initializes Once

No matter how many packages import package c, its init() runs exactly once. Go tracks initialization state per package.


Package-Level Variable Initialization Order

Variables are initialized in declaration order, but the compiler resolves dependencies.

package main

var (
    a = b + 1  // depends on b -- initialized second
    b = 1      // no dependencies -- initialized first
    c = a + b  // depends on a and b -- initialized third
)

func main() {
    fmt.Println(a, b, c) // 2 1 3
}

The compiler performs a topological sort of variable dependencies. A cycle is a compile error:

var x = y + 1
var y = x + 1 // compile error: initialization cycle

Blank Imports for Side Effects

The blank import (_ "package") imports a package solely for its init() side effects — the package name is not used.

import (
    "database/sql"
    _ "github.com/lib/pq"            // registers PostgreSQL driver
    _ "github.com/go-sql-driver/mysql" // registers MySQL driver
    _ "image/png"                      // registers PNG decoder
    _ "image/jpeg"                     // registers JPEG decoder
    _ "net/http/pprof"                 // registers pprof HTTP handlers
    _ "expvar"                         // registers /debug/vars HTTP handler
)

How Database Driver Registration Works

// Inside github.com/lib/pq (simplified)
package pq

import "database/sql"

func init() {
    sql.Register("postgres", &Driver{})
}

// Your code -- now "postgres" driver is available
func main() {
    db, err := sql.Open("postgres", "host=localhost dbname=mydb")
    // ...
}

This is Go's plugin registration pattern: a package registers itself with a central registry during init(), and consumers use the registry by name.


Common init() Use Cases

// 1. Register implementations
func init() {
    sql.Register("postgres", &Driver{})
    image.RegisterFormat("png", "\x89PNG", Decode, DecodeConfig)
}

// 2. Initialize package-level state
var templates *template.Template

func init() {
    templates = template.Must(template.ParseGlob("templates/*.html"))
}

// 3. Validate configuration
func init() {
    if os.Getenv("DATABASE_URL") == "" {
        log.Fatal("DATABASE_URL environment variable required")
    }
}

// 4. Set default values
var logger *slog.Logger

func init() {
    logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))
}

init() vs Constructor Pattern

// init() approach -- runs automatically, hard to test
var db *sql.DB

func init() {
    var err error
    db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err) // crashes entire program
    }
}

// Constructor approach -- explicit, testable, preferred for most cases
type App struct {
    db *sql.DB
}

func NewApp(dsn string) (*App, error) {
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, fmt.Errorf("opening database: %w", err)
    }
    return &App{db: db}, nil
}

Quick Reference

Concept Details
Signature func init() — no arguments, no return
Per file Multiple init() per file allowed
Per package Multiple init() across files allowed
Execution order Imported packages → package vars → init() functions → main()
File order Alphabetical by source file name within a package
Within file Top-to-bottom declaration order
Called once Each package's init() runs exactly once, regardless of import count
Cannot be called init() cannot be called explicitly — only the runtime invokes it
Blank import _ "pkg" — import solely for side effects

Best Practices

  1. Keep init() simple — registration, setting defaults, validating environment; avoid complex logic
  2. Prefer constructors over init() for anything that can fail — constructors return errors, init() can only log.Fatal or panic
  3. Use blank imports at the top of main — make it clear which packages are imported for side effects
  4. Don't rely on cross-file init order — if two init() functions depend on each other, restructure so one calls the other explicitly
  5. Avoid I/O in init() — file reads, network calls, and database connections in init() make testing and startup debugging hard
  6. Document blank imports — add a comment explaining why each _ "pkg" import exists
  7. One init() per file — multiple init() per file is legal but reduces clarity; split into multiple files if needed
  8. Don't set up global mutable state unless it's truly needed — prefer dependency injection

Common Pitfalls

Panicking in init()

func init() {
    data, err := os.ReadFile("config.json")
    if err != nil {
        panic(err) // crashes program before main() even starts
    }
}
If init() panics, the program dies with no opportunity to handle the error gracefully. Prefer constructor functions that return errors.

Implicit Dependencies Between init() Functions

// a.go
var registry = map[string]Handler{}
func init() { /* populate registry */ }

// b.go
func init() {
    handler := registry["user"] // depends on a.go init running first!
}
Cross-file init() dependencies are fragile. If b.go runs before a.go (lexicographic order), the registry is empty.

Testing Code with init() Side Effects

func init() {
    db = connectToProductionDB() // runs during tests too!
}
init() runs for every test that imports the package. This makes tests slow, flaky, and dependent on external resources. Use dependency injection instead.

Blank Import Without Comment

import (
    _ "github.com/lib/pq" // WHY? What does this do?
)
Always comment blank imports:
import (
    _ "github.com/lib/pq" // registers "postgres" database driver
)

init() Cannot Be Called or Referenced

func init() { /* setup */ }

func main() {
    init() // compile error: undefined: init
}
init() is special — it cannot be called, referenced, or assigned. Only the Go runtime invokes it.


Performance Considerations

Scenario Impact
Startup time All init() functions run sequentially before main() — heavy init slows startup
Import chain Deeply nested imports cause cascading init() calls — audit with go mod graph
Database in init Network calls in init() add seconds to startup and break tests
Template parsing Parsing templates in init() is acceptable — it's a one-time cost
Global sync.Once For lazy initialization, sync.Once defers cost to first use instead of startup
Blank imports Only import for side effects when you actually use the registered functionality
// Lazy initialization as an alternative to init()
var (
    dbOnce sync.Once
    db     *sql.DB
)

func getDB() *sql.DB {
    dbOnce.Do(func() {
        var err error
        db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
        if err != nil {
            log.Fatal(err)
        }
    })
    return db
}

Interview Tips

Interview Tip

"What is init() in Go?" init() is a special function that runs automatically when a package is loaded, before main(). It takes no arguments and returns nothing. A package can have multiple init() functions, even in the same file. It's commonly used for registering drivers, validating configuration, and setting up package-level state.

Interview Tip

"What's the initialization order in Go?" First, imported packages are initialized recursively (depth-first). Then package-level variables are initialized in declaration order (with dependency resolution). Then init() functions run in source file order (alphabetical), top-to-bottom within each file. Finally, main() runs. Each package initializes exactly once.

Interview Tip

"What is a blank import?" The syntax _ "pkg" imports a package purely for its side effects — typically its init() function. The most common example is database drivers: _ "github.com/lib/pq" registers the PostgreSQL driver with database/sql so you can use sql.Open("postgres", ...).

Interview Tip

"When should you avoid init()?" Avoid init() for anything that can fail (I/O, network, database connections), anything that makes testing harder (global mutable state), or complex logic that should be explicit. Prefer constructor functions (NewXxx) that return errors and support dependency injection.

Interview Tip

"How does the database driver registration pattern work?" The driver package (e.g., lib/pq) calls sql.Register("postgres", &Driver{}) in its init(). Your code does _ "github.com/lib/pq" to trigger that init(), then uses sql.Open("postgres", dsn). The database/sql package looks up "postgres" in its internal registry to find the driver.


Key Takeaways

  • init() runs automatically before main() — no arguments, no return value
  • Multiple init() per file and per package are allowed — they run in declaration and file order
  • Initialization order: imported packages → package variables → init() → main()
  • Each package initializes exactly once, regardless of how many packages import it
  • Blank imports (_ "pkg") are for side effects — commonly database driver registration
  • Prefer constructors over init() for anything that can fail or needs testing
  • init() cannot be called, referenced, or assigned — it's runtime-only
  • Heavy work in init() slows program startup — consider sync.Once for lazy initialization
  • Always comment blank imports to explain why the side effect is needed