Go init Function: Package Initialization

Go's `init()` function is a special function that executes automatically during package initialization, before your `main()` function runs. Unlike regular functions, you never call `init()`...

Key Insights

  • The init() function executes automatically before main() in a deterministic order: imported packages first, then package-level variables, then all init() functions in the package
  • Multiple init() functions can exist in a single package or file, executing in the order they appear in source files (alphabetically by filename)
  • While useful for database driver registration and one-time setup, init() functions should remain simple and avoid complex logic that makes code harder to test and debug

Introduction to init Functions

Go’s init() function is a special function that executes automatically during package initialization, before your main() function runs. Unlike regular functions, you never call init() explicitly—the Go runtime handles this for you.

You need init() when you have setup code that must run exactly once before your program starts executing. This includes registering database drivers, loading configuration, initializing package-level variables that require computation, or setting up global state.

Here’s the simplest example:

package main

import "fmt"

func init() {
    fmt.Println("Initialization complete")
}

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

// Output:
// Initialization complete
// Main function executing

The init() function has no parameters and no return values. This design choice reflects its purpose: automatic, unconditional initialization that happens before your program logic begins.

Init Function Execution Order

Understanding execution order is critical when working with init() functions. Go follows a deterministic initialization sequence:

  1. Import all packages (recursively)
  2. Initialize package-level variables in dependency order
  3. Execute all init() functions

You can have multiple init() functions in a single file, and they execute in the order they appear:

package main

import "fmt"

func init() {
    fmt.Println("First init in file")
}

func init() {
    fmt.Println("Second init in file")
}

func init() {
    fmt.Println("Third init in file")
}

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

// Output:
// First init in file
// Second init in file
// Third init in file
// Main

When you have multiple files in the same package, Go processes them alphabetically by filename:

// a_setup.go
package config

import "fmt"

func init() {
    fmt.Println("Init from a_setup.go")
}

// z_finalize.go
package config

import "fmt"

func init() {
    fmt.Println("Init from z_finalize.go")
}

// Output:
// Init from a_setup.go
// Init from z_finalize.go

For imported packages, initialization follows the dependency graph. Consider this structure:

// package database
package database

import "fmt"

func init() {
    fmt.Println("Database package initialized")
}

// package logger
package logger

import "fmt"

func init() {
    fmt.Println("Logger package initialized")
}

// main package
package main

import (
    "fmt"
    _ "yourapp/database"
    _ "yourapp/logger"
)

func init() {
    fmt.Println("Main package initialized")
}

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

// Output:
// Database package initialized
// Logger package initialized
// Main package initialized
// Main function

Common Use Cases

The most common legitimate use of init() is database driver registration. The standard library’s database/sql package uses this pattern:

package main

import (
    "database/sql"
    _ "github.com/lib/pq" // PostgreSQL driver registers itself in init()
)

// Inside the pq package:
// func init() {
//     sql.Register("postgres", &Driver{})
// }

func main() {
    db, err := sql.Open("postgres", "connection-string")
    // Use database
}

The blank import (_) ensures the package’s init() function runs even though you don’t directly use any exported identifiers.

Loading configuration from environment variables is another practical use:

package config

import (
    "log"
    "os"
    "strconv"
)

var (
    DatabaseURL string
    Port        int
    Debug       bool
)

func init() {
    DatabaseURL = os.Getenv("DATABASE_URL")
    if DatabaseURL == "" {
        log.Fatal("DATABASE_URL must be set")
    }

    portStr := os.Getenv("PORT")
    if portStr == "" {
        Port = 8080 // default
    } else {
        var err error
        Port, err = strconv.Atoi(portStr)
        if err != nil {
            log.Fatalf("Invalid PORT value: %v", err)
        }
    }

    Debug = os.Getenv("DEBUG") == "true"
}

Logger initialization is also common:

package logger

import (
    "log"
    "os"
)

var Log *log.Logger

func init() {
    Log = log.New(os.Stdout, "[APP] ", log.LstdFlags|log.Lshortfile)
}

Init vs. Main: Key Differences

The init() and main() functions serve different purposes and have distinct characteristics:

Feature init() main()
Execution Before main, automatically Entry point, explicit
Quantity Multiple per package One per program
Parameters None None
Return value None None
Package Any package Only main package

Here’s a demonstration:

package main

import (
    "fmt"
    "time"
)

var startTime time.Time

func init() {
    startTime = time.Now()
    fmt.Println("Init: Recording start time")
}

func init() {
    fmt.Println("Init: Performing additional setup")
}

func main() {
    fmt.Println("Main: Starting application")
    fmt.Printf("Time since init: %v\n", time.Since(startTime))
}

// Output:
// Init: Recording start time
// Init: Performing additional setup
// Main: Starting application
// Time since init: 123.456µs

Use init() for package-level initialization that must happen automatically. Use main() as your program’s entry point where you orchestrate application logic.

Best Practices and Anti-patterns

Keep init() functions simple and fast. Avoid heavy computation, network calls, or complex logic that could fail:

Bad: Heavy computation in init

package cache

import "time"

var lookupTable map[string]int

func init() {
    // DON'T: Expensive computation during initialization
    lookupTable = make(map[string]int)
    for i := 0; i < 10000000; i++ {
        lookupTable[fmt.Sprintf("key-%d", i)] = i
        time.Sleep(time.Microsecond) // Simulating slow operation
    }
}

Good: Lightweight initialization

package cache

var lookupTable map[string]int

func init() {
    // DO: Simple initialization
    lookupTable = make(map[string]int, 1000)
}

// Provide explicit function for expensive operations
func BuildLookupTable() error {
    for i := 0; i < 10000000; i++ {
        lookupTable[fmt.Sprintf("key-%d", i)] = i
    }
    return nil
}

Handle errors appropriately. Since init() can’t return errors, you have two options: panic for truly unrecoverable errors, or store the error for later:

package config

import (
    "errors"
    "os"
)

var (
    APIKey string
    initErr error
)

func init() {
    APIKey = os.Getenv("API_KEY")
    if APIKey == "" {
        initErr = errors.New("API_KEY environment variable required")
    }
}

func Validate() error {
    return initErr
}

// In main:
// if err := config.Validate(); err != nil {
//     log.Fatal(err)
// }

Avoid init functions that depend on order of execution within the same package. While Go guarantees the order, it makes code fragile and hard to understand.

Testing Considerations

Init functions create testing challenges because they run automatically before every test. This can cause issues with global state and make tests interdependent.

Consider this problematic code:

package metrics

import "sync"

var (
    counter int
    mu      sync.Mutex
)

func init() {
    counter = 100 // Starting value
}

func Increment() {
    mu.Lock()
    counter++
    mu.Unlock()
}

func GetCount() int {
    mu.Lock()
    defer mu.Unlock()
    return counter
}

Tests for this code might interfere with each other because counter maintains state across tests.

Refactor to make initialization explicit:

package metrics

import "sync"

type Counter struct {
    value int
    mu    sync.Mutex
}

func NewCounter(initial int) *Counter {
    return &Counter{value: initial}
}

func (c *Counter) Increment() {
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
}

func (c *Counter) Get() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

Now each test can create its own Counter instance with a clean state:

func TestIncrement(t *testing.T) {
    c := NewCounter(100)
    c.Increment()
    if got := c.Get(); got != 101 {
        t.Errorf("got %d, want 101", got)
    }
}

This approach gives you control over initialization, makes dependencies explicit, and keeps tests isolated. Reserve init() for truly package-level concerns like driver registration, not for setting up testable business logic.

The init() function is a powerful tool, but power requires discipline. Use it sparingly for legitimate initialization needs, keep implementations simple, and always consider the testability implications of your design choices.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.