What runs between a Go binary starting and the first statement of main.main?
answer
- your main is not the first code
- three phases, and you write the last one
- imports are initialised before importers
- package vars first, then init functions
- one goroutine does all of it, then main
basics
~20 sThe Go runtime starts first: it sets up the heap, the goroutine scheduler and the garbage collector, then creates the main goroutine. That goroutine initialises every imported package - package-level variables, then init functions - and only then calls main.main.
solid answer
~40 sStartup has three phases. First the runtime bootstraps itself: memory allocator, goroutine scheduler, signal handling, background monitor thread, and it reads its own settings such as `GOGC` and `GOMAXPROCS`. It then creates the main goroutine, which runs `runtime.main`. That enables the garbage collector and runs package initialisation: for every package in the import graph, its package-level variables are initialised and then its `init` functions run, with each package fully initialised before the packages that import it, so `main` is initialised last. All of this is sequential on one goroutine. Only when the whole graph is initialised does `runtime.main` call `main.main`. The practical consequence is that plenty of your code can already have run - and can already have panicked - before the first line of `main` executes.
code
go · 16 linesvar a = trace("package var")
func init() {
fmt.Println("init")
}
func main() {
fmt.Println("main")
}
func trace(s string) string {
fmt.Println(s)
return s
}
// prints: package var, then init, then maingo deeper
Be ready to name the order out loud: runtime setup, then package-level variables and init functions for every imported package, then main. Knowing that main is not the first code to run is the whole point of the question.
Explain the mechanics: initialisation is sequential on the main goroutine, dependency-ordered so imports finish before importers, and the scheduler and collector are already live, so init can allocate, lock and start goroutines.
Show that you treat pre-main work as production surface: it delays readiness, it cannot log through a logger main has not built, and it can panic before any health endpoint exists. Mention measuring it with GODEBUG=inittrace=1.
Own the policy question: how much of a binary's behaviour is allowed to be decided by the import graph rather than by explicit wiring, and what that costs in startup latency, testability and reviewability across many services.
## The entry point is not `main` A compiled Go binary's entry point is a small runtime stub, not your `main` function. By the time `main.main` executes, three distinct phases have already completed. ### Phase 1 - runtime bootstrap The runtime prepares the world your code will run in: - **The memory allocator**: heap arenas and size classes are set up, so allocation works. - **The goroutine scheduler**: the runtime creates the `P`s (the logical processors that run goroutines), bounded by `GOMAXPROCS`, and the OS threads to drive them. - **Process inputs**: command-line arguments and the environment are captured, which is why `os.Args` and `os.Getenv` already work during package initialisation. - **Runtime configuration**: `GOGC`, `GOMEMLIMIT`, `GODEBUG` and friends are read here. - **Signal handling and the background monitor thread**, which later handles preemption and network polling. The runtime then creates the **main goroutine** and schedules it. That goroutine runs `runtime.main`, and everything below happens inside it. ### Phase 2 - package initialisation `runtime.main` enables the garbage collector and then initialises every package in the program. For each package: 1. Its **package-level variables** are initialised - evaluating whatever expression is on the right of the `=`. 2. Its **`init` functions** run. Packages are initialised in **dependency order**: a package is completely initialised before any package that imports it starts, and `main` is therefore initialised last. Every bit of it runs **sequentially on the single main goroutine** - there is no parallel initialisation, and no `init` overlaps with another. ### Phase 3 - `main.main` Only once the entire import graph is initialised does the runtime call your `main` function. ## What follows from this shape **Code runs before `main`, whether you meant it to or not.** Importing a package pays for its initialisation even if you never call anything in it - and that includes packages it imports transitively. **`init` has no signature to fail through.** It takes no parameters and returns nothing, so an initialisation error has only two outlets: panic, or silently leave a variable at its zero value. A panic here kills the process before any logging, metrics or health endpoint that `main` would have set up exists. **The runtime is fully alive during initialisation.** The garbage collector is running, so allocations in `init` are ordinary heap allocations; the scheduler is running, so an `init` function may legitimately start goroutines, use channels and take locks. It may also block forever, in which case the program hangs and never reaches `main`. **Nothing has parsed your flags.** `os.Args` is populated, but `flag.Parse` is a call somebody has to make - conventionally from `main` - so an `init` that reads a flag's value sees the default. **Anything `main` does happens strictly after all of it.** If `main` reads a config file, opens a database handle or sets up a logger, no package-level variable initialised earlier can have seen the result. ## Seeing it happen Running a binary with `GODEBUG=inittrace=1` makes the runtime print one line per package to standard error as that package's initialisation completes, with the elapsed time since process start, the time spent in that package, and how much it allocated. It is the cheapest way to see exactly how much of your program runs before `main`. ## A note on `GOMAXPROCS` The scheduler's width comes from `GOMAXPROCS`, which defaults to the number of usable CPUs. Since Go 1.25, on Linux that default also honours the cgroup CPU bandwidth limit and is re-read periodically while the program runs, so a containerised service no longer needs to set it by hand in the common case.
- Is the garbage collector running while package init functions execute?Yes. The runtime enables the collector before it runs package initialisation, so allocations made in `init` are ordinary heap allocations and can be collected like any other. `GODEBUG=inittrace=1` even reports the bytes and allocation count each package's initialisation cost.
- Can an init function start a goroutine, and will it run before main?Yes - the scheduler is already running, so the goroutine is created and scheduled normally. But nothing waits for it: `main.main` may start while that goroutine is still working, so treat it as background work with no completion guarantee unless you synchronise explicitly.
- What happens if an init function panics?It is an ordinary panic on the main goroutine, so it unwinds and terminates the process before `main.main` is ever called. A `defer` with `recover` inside that same `init` function can catch it, but by then you have no logger, no configuration and no server, which is why fallible startup work belongs in `main` instead.
saying these in an interview costs you the question
- Says main is the first code the process runs
- Thinks package-level variables are computed lazily on first read
- Believes init functions run in parallel goroutines
- Assumes command-line flags are already parsed during init
- Thinks the garbage collector only starts once main begins