On which goroutine do Go's package init functions run, and what happens if one blocks?
answer
- count the goroutines doing this work
- the same goroutine that later calls main
- sequential, so the costs add up
- the runtime is live, so waiting is possible
- no timeout, no supervisor, no second goroutine
basics
~20 sAll package initialisation runs sequentially on the main goroutine, before main.main is called. If one init function blocks - on a channel, a lock or a network call - startup stops there: the process stays alive but main never runs.
solid answer
~50 sThere is exactly one initialisation goroutine: the main goroutine runs every package's variable initialisers and `init` functions one after another, and only then calls `main.main`. Nothing about initialisation is concurrent, so a slow `init` delays the whole program and a blocking `init` stops it dead - the process is alive, consuming no CPU, with `main` never reached and no server, logger or health endpoint up, because those are things `main` would have created. The runtime is fully live at this point, so an `init` *may* start goroutines and use channels, and that is exactly how you get into trouble: it can wait on a channel nobody sends to, or on a goroutine that itself waits for something `main` was going to do. The rule of thumb is that anything in `init` must be non-blocking and finite.
code
go · 5 linesvar ch = make(chan int)
func init() {
<-ch // no sender: startup stops here and main.main is never reached
}go deeper
Remember the single fact that carries the answer: all init work happens one after another on the main goroutine before main is called. Nothing runs it in parallel and nothing rescues it if it waits.
Explain why blocking is even possible - the scheduler, channels and network are already live during initialisation - and that initialisation time is additive across every imported package.
Talk about the operational shape: a hang before main looks like a dead container with no logs, and the fix is to move anything that can wait into a function main calls with a context and an error return.
Frame it as a constraint you impose on every package the organisation writes: import-time code may compute but may not wait, because a waiting import is a startup failure mode nobody can configure, cancel or observe.
## One goroutine, in sequence After the runtime bootstraps, it creates the main goroutine and hands it the job of initialising the program. That single goroutine walks the import graph in dependency order and, for each package, evaluates the package-level variable initialisers and then runs the package's `init` functions. Only when the last package - `main` - is done does it call `main.main`. Two properties matter, and both follow from *sequential on one goroutine*: - **Initialisation cost is additive.** If eleven packages each spend 100ms in `init`, your binary takes over a second to reach `main`. There is no parallelism to hide it. - **A blocked `init` blocks the program.** There is no timeout, no supervisor and no second initialisation goroutine to make progress. ## Why an init function can block at all By the time `init` runs, the scheduler and the garbage collector are already up. Channels work. Mutexes work. `go` statements work. Network calls work. From inside `init` the runtime looks exactly like it does inside `main` - which is precisely why blocking is possible: ```go var ch = make(chan int) func init() { <-ch // nobody will ever send } ``` The process starts, prints nothing, exits nothing, and burns no CPU. From the outside it looks like a hang during startup, and the usual suspects - a slow database, a stuck DNS lookup, a container waiting on a volume - all look identical. Realistic versions of the same shape: - an `init` that dials a service that is not up yet and uses a client with no timeout; - an `init` that reads from a channel expected to be fed by a goroutine another package's `init` starts - but that package is initialised *later*, or not imported at all; - an `init` that acquires a lock a goroutine started by an earlier `init` is holding while it waits for something; - an `init` that waits for work it handed to a goroutine which itself needs configuration `main` has not loaded yet. ## Goroutines started during initialisation Starting a goroutine in `init` is legal and it really does get scheduled - but **nothing waits for it**. `main.main` can begin while that goroutine is still on its first line. So background work started at import time has no completion guarantee and no error path: if it fails, there is no caller to tell. If you *do* make `init` wait for it, you have reintroduced the blocking problem, this time with the ability to deadlock the two against each other. ## What this means in practice The conservative rule that falls out of the mechanics is: **package initialisation may compute, but it may not wait.** Building a lookup table, compiling a pattern, registering an implementation into a registry - all fine, because they finish and cannot hang. Dialling, reading files, taking locks that other initialisation code holds, waiting on channels - all belong in a function that `main` calls, where you have a `context.Context` to cancel, an `error` to return, and a logger to report through. ## Diagnosing it When a binary hangs before producing any output, run it with `GODEBUG=inittrace=1`. The runtime prints a line per package as its initialisation completes, so the *last* line you see names the package that finished immediately before the one that is stuck - which is usually enough to identify the culprit without a debugger.
- If an init function starts a goroutine, is that goroutine guaranteed to have run before main.main?No. The `go` statement only creates and schedules it; `main.main` may be entered while the goroutine has done nothing. There is also no error path back to anyone. If the work must be complete, do it synchronously in a function `main` calls, so you can wait on it and handle failure.
- How would you tell a hang during package initialisation apart from a hang inside main?Run the binary with `GODEBUG=inittrace=1`. Each package prints a line as its initialisation finishes, so if the output stops partway you are stuck in initialisation, and the next package in dependency order after the last printed line is where to look. If every package prints and it still hangs, the problem is in `main`.
- Why is a network call in init worse than the same call made from main?In `init` there is no `context.Context` to cancel it, no `error` to return, no logger or metrics yet, and no way for the caller to retry or degrade. The same call in `main` can be given a deadline, retried, logged, and turned into a clean non-zero exit with a message.
saying these in an interview costs you the question
- Thinks each package is initialised on its own goroutine
- Says the runtime times out or skips a slow init
- Believes goroutines started in init cannot run until main
- Assumes init functions of different packages can overlap
- Thinks a blocking init makes the process exit rather than hang