Go init() Function — Junior Level¶
1. Introduction¶
What is it?¶
init() is a special function in Go that the runtime calls automatically before main() begins. You never call init yourself — the language guarantees it runs exactly once per package, after package-level variables are initialized but before any other code in your program.
It is the standard hook for "setup that must happen before anything else": registering a database driver, registering an image format, validating environment variables, parsing flags, building lookup tables, and so on.
How to use it?¶
package main
import "fmt"
var greeting = "hello"
func init() {
fmt.Println("init runs first; greeting =", greeting)
}
func main() {
fmt.Println("main runs second")
}
Output:
You did not call init. The Go runtime did, automatically, after assigning "hello" to greeting and before entering main.
2. Prerequisites¶
- Functions basics (2.6.1)
- Package basics:
packageclause, importing packages - Variable declarations at package level
- Understanding of
mainpackage vs library packages
3. Glossary¶
| Term | Definition |
|---|---|
| init function | A function named init with no parameters and no return values that the runtime calls automatically |
| package initialization | The phase before main where package-level vars are assigned and init functions run |
| blank import | import _ "path" — imports a package solely for its side effects (its init runs) |
| side-effect import | A blank import whose only purpose is to run that package's init |
| init order | The deterministic sequence in which init functions across the program run |
| package-level vars | Variables declared at file scope outside any function |
| import graph | The directed graph of which packages import which others |
| transitive import | A package imported by something you import, not by you directly |
4. Core Concepts¶
4.1 Defining an init Function¶
The signature is fixed: func init() — no parameters, no return values, no receiver. You can define it in any file of any package.
package config
import "log"
var apiKey string
func init() {
apiKey = "loaded-from-somewhere"
log.Println("config: init complete")
}
Rules at a glance: - Name MUST be exactly init (lowercase). - Signature MUST be func init(). - It cannot be referenced by name in code: you cannot write init(), take its address, or assign it to a variable. - It runs exactly once per package per program, no matter how many files or packages reference your package.
4.2 Multiple init Functions in One File¶
Unlike most languages with "static constructors", Go allows as many init functions as you want in a single file. They run in the order they appear in the source.
package demo
import "fmt"
func init() {
fmt.Println("init A")
}
func init() {
fmt.Println("init B")
}
func init() {
fmt.Println("init C")
}
When this package is loaded, you see:
This is useful for grouping unrelated setup steps without forcing them into one giant function.
4.3 Init Order Across Files in a Package¶
If your package has multiple files, all init functions still run, in a deterministic order: files are presented to the compiler in alphabetical order by filename, and within each file init functions run top-to-bottom.
// a_setup.go
package mypkg
import "fmt"
func init() { fmt.Println("A1") }
func init() { fmt.Println("A2") }
// b_setup.go
package mypkg
import "fmt"
func init() { fmt.Println("B1") }
When mypkg is loaded:
Important: relying on this filename-alphabetical order in production code is fragile. The Go spec guarantees deterministic order, and current toolchains use alphabetical filename order. But cross-file init dependencies are bad design — keep each file's init independent.
4.4 Package-Level Vars Run BEFORE init¶
Before any init runs, every package-level variable is assigned its initializer. This means inside init, you can rely on those vars being ready.
package main
import "fmt"
var greeting = makeGreeting() // runs before init
func makeGreeting() string {
fmt.Println("var initializer")
return "hi"
}
func init() {
fmt.Println("init sees:", greeting)
}
func main() {
fmt.Println("main:", greeting)
}
Output:
The runtime computes a dependency graph among package vars. Vars are initialized in an order that respects those dependencies, then init runs.
4.5 Init Order Across Packages¶
When package main imports package A, and A imports B, the order is: 1. Package B initializes fully (its vars, then its inits). 2. Package A initializes fully (its vars, then its inits). 3. Package main initializes fully (its vars, then its inits). 4. main() runs.
So dependencies init first, depth-first. Each package initializes exactly once, even if imported many times. The full sequence is deterministic, not parallel.
Result:B, then C, then A, then main, then main.main(). 4.6 Blank Imports for Side Effects¶
Sometimes you want a package's init to run, but you don't use any of its exported names. The blank identifier on an import does exactly that:
package main
import (
"database/sql"
_ "github.com/lib/pq" // blank import — registers the postgres driver in init
)
func main() {
db, err := sql.Open("postgres", "...")
_ = db
_ = err
}
Without the blank import, pq is never imported, its init never runs, and sql.Open("postgres", ...) returns "unknown driver". This is the canonical Go pattern for plugin-style registration.
Other classic blank-import examples: - import _ "image/png" — register PNG decoder with the image package. - import _ "net/http/pprof" — install profiling endpoints on the default http mux. - import _ "embed" — historically (now //go:embed directives instead).
4.7 init Has No Parameters and No Return¶
The signature is fixed:
func init() { /* ... */ } // valid
func init() error { return nil } // INVALID — compile error
func init(args []string) { } // INVALID — compile error
func (s Server) init() { } // legal func, but NOT a special init
The last one is the trap: a method named init on a receiver is just a regular method. The runtime ignores it. You cannot make a method into the magic init.
5. Common Mistakes¶
5.1 Calling init() Yourself¶
Theinit identifier is never bound in any scope you can reach. The compiler refuses. 5.2 Adding a Receiver¶
This is just a method namedinit. It is not the magic init. Define a top-level func init(). 5.3 Returning a Value¶
The signature must be exactlyfunc init(). Errors must be handled inside, typically by log.Fatal or by setting a package-level state var that callers check. 5.4 Heavy Work in init¶
var DB *sql.DB
func init() {
var err error
DB, err = sql.Open("postgres", os.Getenv("DSN"))
if err != nil { log.Fatal(err) }
if err := DB.Ping(); err != nil { log.Fatal(err) } // network call!
}
sync.Once (covered at the middle level). 5.5 Order Dependencies Across Files¶
This works because Go's dependency analysis seesCache references rules and orders accordingly. But: Here init in a.go depends on rules from b.go. Since vars init before init, this still works. But: // a.go
func init() { Cache = buildCache() } // expects b's init to have run
// b.go
func init() { setupRules() }
a.go runs first, before setupRules sets the rules. Bug. Fix: don't have init-to-init dependencies; instead express them as package-var dependencies, or use one package per layer. 5.6 Panicking in init¶
A panic ininit aborts the program before main even starts. There is no main to recover. The user sees a goroutine 1 panic stack and exits with code 2. For graceful failure, return errors from a Setup() function called from main. 6. Mini Exercises¶
Exercise 1 — Two Inits in One File¶
Write a single main.go with two init functions and main. Print "first", "second", "main". Verify the order.
Solution
Exercise 2 — Var Then Init¶
Declare a package-level var x = 10. In init, print x. In main, print x. Both should print 10.
Solution
Exercise 3 — Side-Effect Import¶
Create two files: mypkg/mypkg.go with func init() { fmt.Println("mypkg!") } and main.go that does nothing but import _ "yourmodule/mypkg". Run and confirm "mypkg!" prints.
Solution
Running prints `mypkg!` because the blank import forces `mypkg`'s init.Exercise 4 — Failed init by Removing Blank Import¶
Take a small program using database/sql with a postgres DSN, and a blank import of github.com/lib/pq. Remove the blank import. Observe sql: unknown driver "postgres". Restore it.
Exercise 5 — Init in Two Packages¶
Make package a import package b. Both have init functions printing their names. The main package imports a. Run and observe b, then a, then main.
7. Cheat Sheet¶
| Scenario | Code | Notes |
|---|---|---|
| Define init | func init() { ... } | No params, no return |
| Multiple inits, one file | Just declare more func init() | Run in source order |
| Order across files | Alphabetical filename order | Don't rely on it for logic |
| Order across packages | Imported packages first, depth-first | Each package once |
| Run an init you don't import names from | import _ "path" | Blank import |
Init for database/sql driver | import _ "github.com/lib/pq" | Driver init calls sql.Register |
| Init for image decoder | import _ "image/png" | Decoder init calls image.RegisterFormat |
| Init signature must be | func init() | Anything else is not the magic init |
| Init with receiver | func (T) init() | This is a normal method, NOT auto-called |
| Heavy work in init | Avoid | Use sync.Once for lazy initialization |
| Panic in init | Aborts program | No way to recover |
Mental Model¶
program start
├── load package main and recursively all imports
├── for each package, deepest first:
│ ├── assign package-level vars (in dependency order)
│ └── run init functions (in source/file order)
└── call main.main()
Common Pitfalls Recap¶
- "I can't add a parameter to init." Correct — fixed signature.
- "Can I call init from main?" No.
- "Can I return an error from init?" No — the signature forbids it.
- "Why do I see 'unknown driver'?" Missing blank import for the driver.
- "Why is my test slow?" Likely
initdoing heavy I/O. Refactor tosync.Onceor explicit setup.
You now have the foundation. The middle level shows real-world patterns: registry initialization, lazy alternatives, testability, and when not to use init.
8. Extended Walkthrough — A Complete Example¶
Let's trace through a small program step by step so you can see exactly when everything happens.
8.1 The Source¶
// format/format.go
package format
import "fmt"
var Prefix string
func init() {
fmt.Println("[1] format.init: setting Prefix")
Prefix = ">> "
}
func With(s string) string {
return Prefix + s
}
// greetings/greetings.go
package greetings
import (
"fmt"
"example/initdemo/format"
)
var greeting = format.With("hello")
func init() {
fmt.Println("[2] greetings.init: greeting is", greeting)
}
func Greet() string {
return greeting
}
// main.go
package main
import (
"fmt"
"example/initdemo/greetings"
)
var bigPrint = func() string {
fmt.Println("[3] main package var initializer")
return greetings.Greet()
}()
func init() {
fmt.Println("[4] main.init: bigPrint is", bigPrint)
}
func main() {
fmt.Println("[5] main.main begins")
fmt.Println(" Greet says:", greetings.Greet())
}
8.2 Predicted Output¶
[1] format.init: setting Prefix
[2] greetings.init: greeting is >> hello
[3] main package var initializer
[4] main.init: bigPrint is >> hello
[5] main.main begins
Greet says: >> hello
8.3 Step-by-Step Trace¶
The runtime processes the import graph: - main imports greetings. - greetings imports format. - format imports nothing.
Order is depth-first by post-order: format, then greetings, then main.
Within format: 1. Package vars: Prefix declared but its initializer is the empty string default. 2. init runs: prints "[1]" line, sets Prefix = ">> ".
Within greetings: 3. Package vars: greeting = format.With("hello") → uses format.Prefix (which is ">> ") → greeting = ">> hello". 4. init runs: prints "[2]" line.
Within main: 5. Package var bigPrint evaluates its initializer (an immediately-called function literal). Prints "[3]". Returns greetings.Greet() = ">> hello". 6. main.init runs: prints "[4]". 7. main.main runs: prints "[5]" and the greeting.
The order is rigorously determined by the rules: - Imported packages init fully before the importer. - Within a package, vars init before init functions. - Package var initializers run in dependency order.
8.4 What Happens If You Reorder?¶
If you switch the order of imports in main.go:
If you remove greetings from main.go (but main.go doesn't use it), the entire greetings and format initialization sequence is gone. The binary is smaller. This is sometimes called "tree-shaking by import" — Go only links packages reachable from main.
If you blank-import:
greetings and (transitively) format still init. You just don't bind greetings in your namespace. 9. The init "Lifecycle"¶
Visually:
[Compile time]
- The Go compiler reads all .go files for the package.
- It synthesizes a per-package init wrapper that:
1. Initializes package-level vars in topological order.
2. Calls each user-defined `func init()` in source order.
- It records the package's import dependencies in metadata.
[Link time]
- The linker arranges packages in dependency order.
- It produces a binary with an `inittask` table.
[Runtime startup]
- runtime.main():
1. Walks the inittask table in dependency order.
2. For each package: var init, then user init functions.
3. Calls main.main().
[Test runtime]
- For `go test`, the test binary is the same as a regular binary
plus generated test scaffolding. All inits still run.
- TestMain (if defined) runs after all inits.
This is why init is a build-time concept that manifests at runtime startup: the compiler decides, the runtime executes.
10. Frequently Asked Questions¶
Q: Can init be in a package with no exported names?
A: Yes — that's the whole point of side-effect-only packages. Tools like database drivers and codecs do exactly this. The package has unexported state and an init that mutates a global registry in a different package.
Q: Does init run if I don't use any of the package's exports?
A: It runs if the package is linked into your binary. Importing a package (even blank) links it. If you don't import it at all (directly or transitively), it doesn't run because it isn't in the binary.
Q: What if I have init in two different packages and they both want to set a default?
A: Whichever runs second wins. Since order across packages is determined by imports, you can structure it intentionally — but if they're sibling packages, the order can be unpredictable. Better: have the consumer package (the one that uses the default) define it, and let plugins override.
Q: Can main call init?
A: No. The init identifier is unbound in your scope. The compiler says undefined: init.
Q: If I rename a _test.go file, does the init order in tests change?
A: Possibly, since file order is what determines cross-file init order. But init in _test.go files participates in the same init order as .go files of that package's test build. Don't depend on the order.
Q: Does init work in internal packages?
A: Yes. internal is purely a visibility rule, not an init rule. Init functions in internal packages run normally.
Q: What about _ (underscore) functions?
A: _ is the blank identifier. You cannot name a function _:
_ as a parameter name to ignore it. That's unrelated to init. 11. Drill — Predict the Output¶
Try to predict the output of each snippet without running it. Then verify.
Drill 1¶
package main
import "fmt"
var x = "X"
func init() { x = "init1:" + x }
func init() { x = "init2:" + x }
func main() { fmt.Println(x) }
Answer
`init2:init1:X` — vars first, then inits in source order, each transforms the value.Drill 2¶
package main
import "fmt"
func init() { fmt.Println("a") }
var x = func() int { fmt.Println("b"); return 0 }()
func main() {}
Drill 3¶
package main
import "fmt"
var n = 10
func init() {
if n != 10 { panic("oh no") }
n = 20
}
func main() { fmt.Println(n) }
Answer
`20` — init verifies var, then mutates it. Main sees the post-init value.Drill 4¶
// file: aa.go
package main
import "fmt"
func init() { fmt.Println("aa") }
// file: zz.go
package main
import "fmt"
func init() { fmt.Println("zz") }
// file: main.go
package main
func main() {}
Drill 5¶
package main
import "fmt"
var greeting = compute()
func compute() string {
fmt.Println("compute called")
return "hello"
}
func init() { fmt.Println("init:", greeting) }
func main() { fmt.Println("main:", greeting) }
Answer
Var initializer (with `compute()` call) runs first, then init, then main.If you predicted all five correctly, your mental model of init/var ordering is solid. If not, re-read sections 4 and 8 — the trace there walks through the same rules.