skip to content

Program Startup and Exit

The runtime's work around your code: setup and package init before main, the fatal errors it raises instead of panicking, and the several different ways a Go process can stop.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

13

What runs between a Go binary starting and the first statement of main.main?

level: juniorimportance: must knowfreq 68%

answer

  1. your main is not the first code
  2. three phases, and you write the last one
  3. imports are initialised before importers
  4. package vars first, then init functions
  5. one goroutine does all of it, then main

basics

~20 s

The 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 s

Startup 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 lines
go
var 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 main

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

In Go, what happens to deferred functions and buffered output when a program calls os.Exit?

level: juniorimportance: must knowfreq 65%

basics

~10 s

os.Exit ends the process immediately. Deferred functions never run, so anything a defer would have flushed or closed is lost: bytes still sitting in a bufio.Writer never reach the file or the terminal.

open as a page

What does Go's `fatal error: all goroutines are asleep - deadlock!` actually mean?

level: juniorimportance: must knowfreq 60%

basics

~10 s

Go's scheduler found nothing left to run: every goroutine is parked on a channel operation, a lock or a similar wait, so none can ever wake another. The runtime aborts the whole program immediately.

open as a page

When the Go runtime throws `fatal error: concurrent map writes`, why does no deferred function run?

level: middleimportance: must knowfreq 52%

basics

~20 s

Because it is not a panic. A fatal runtime error skips stack unwinding entirely: the runtime freezes the other goroutines, prints the banner and every goroutine's stack, and exits. Deferred calls and recover never enter the picture.

open as a page

On which goroutine do Go's package init functions run, and what happens if one blocks?

level: middleimportance: should knowfreq 44%

basics

~20 s

All 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.

open as a page

In Go, why is a package-level var that reads a config value zero when main.main loads that config?

level: middleimportance: should knowfreq 52%

basics

~20 s

Package-level variables and init functions all finish before main.main is called, so the variable was computed while the config was still empty and captured a zero value. Nothing recomputes it later when main fills the config in.

open as a page

How does runtime.Goexit differ from returning from a goroutine's function, and from os.Exit?

level: middleimportance: should knowfreq 38%

basics

~20 s

runtime.Goexit ends the calling goroutine from anywhere in its stack, running every pending deferred call on the way out; recover returns nil, since this is not a panic. os.Exit instead ends the whole process and runs no defers.

open as a page

A Go migration tool writes its report through a bufio.Writer, and on failed CI runs the report is truncated — how do you find the cause?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Suspect an error path that calls os.Exit or log.Fatal, so the deferred Flush never runs and buffered bytes are dropped. The tell is that successful runs produce a complete report while failing runs stop mid-buffer.

open as a page

Why does Go's deadlock detector stay silent when a production service hangs with every worker blocked?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The runtime aborts only when nothing at all could run again. A server always has a goroutine parked on network I/O, in a syscall or on a timer, so the terminal condition never holds and the process hangs quietly. Read the goroutine stacks instead.

open as a page

What rule should your team's Go standard set for what init() may do before main?

level: principalimportance: should knowfreq 36%

basics

~20 s

Set a narrow rule: init may only do work that is fast, cannot fail and cannot block, such as building tables or registering into the module's own registry. Anything with I/O or an error belongs in a constructor main calls.

open as a page

Which Go runtime failures print `fatal error:` instead of raising a recoverable panic?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

Concurrent map access, the all-goroutines-asleep deadlock, out of memory, stack overflow and unlocking an unlocked mutex are fatal. Nil dereferences, index out of range, failed type assertions, divide by zero and nil-map writes are ordinary panics.

open as a page

A Go service takes seconds to reach main.main - how do you find which package init is slow?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Run the binary with GODEBUG=inittrace=1. The runtime prints one line per package to standard error as its initialisation finishes, with elapsed time since start, time spent in that package, and bytes and allocations - so the expensive package names itself.

open as a page

What rule would you set for os.Exit and log.Fatal in shared Go packages, and how would you enforce it?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Confine process exit to package main. A shared package calling os.Exit or log.Fatal takes a whole-program decision away from every caller and skips their cleanup, so libraries return errors and one place in main flushes, then exits with a status.

open as a page