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:
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¶
- Keep
init()simple — registration, setting defaults, validating environment; avoid complex logic - Prefer constructors over
init()for anything that can fail — constructors return errors,init()can onlylog.Fatalorpanic - Use blank imports at the top of
main— make it clear which packages are imported for side effects - Don't rely on cross-file init order — if two
init()functions depend on each other, restructure so one calls the other explicitly - Avoid I/O in
init()— file reads, network calls, and database connections ininit()make testing and startup debugging hard - Document blank imports — add a comment explaining why each
_ "pkg"import exists - One
init()per file — multipleinit()per file is legal but reduces clarity; split into multiple files if needed - 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
}
}
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!
}
init() dependencies are fragile. If b.go runs before a.go (lexicographic order), the registry is empty.
Testing Code with init() Side Effects
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
Always comment blank imports:init() Cannot Be Called or Referenced
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 beforemain()— 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 — considersync.Oncefor lazy initialization - Always comment blank imports to explain why the side effect is needed