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 beforemain()in a deterministic order: imported packages first, then package-level variables, then allinit()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:
- Import all packages (recursively)
- Initialize package-level variables in dependency order
- 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.