What happens to still-running goroutines when a Go program's main function returns?
answer
- the main goroutine decides the process lifetime
- nothing waits for anybody
- their deferred calls never run
- a sleep at the bottom of main is a guess
basics
~20 sThe process exits immediately and every other goroutine is killed where it stands. Their deferred calls never run and their output may never appear. Nothing waits for them, so main must wait explicitly if the work matters.
solid answer
~40 sReturning from `main` ends the program. The main goroutine's own deferred calls run, then the process exits with status 0, and every other goroutine is stopped mid-instruction — no unwinding, no deferred calls, no chance to flush a buffer or finish a write. This is why a first Go program that starts goroutines in a loop and then falls off the end of `main` often prints nothing at all: the process was gone before the scheduler got round to them. There is no runtime-level join, so if `main` must not exit yet, the code that started the goroutines has to wait for them — typically by receiving from a channel each goroutine signals on, or with a `sync.WaitGroup`. Waiting is the starter's job, never the runtime's.
code
go · 6 linesfunc main() {
for _, h := range hosts {
go check(h) // check prints one line per host
}
// main returns here: the process exits and check may never run
}go deeper
Remember the rule and the symptom: main returning ends the process, so a program that starts goroutines and then finishes often prints nothing. Know that you must wait on something rather than hope.
Explain what is lost — deferred calls, buffered output — and show the channel or WaitGroup shape that waits properly. Be able to say why a sleep is a guess rather than a fix.
Talk about shutdown as a design concern: which goroutines must be drained before exit, which are genuinely disposable, and how you make that explicit so cleanup is not left to luck at process end.
Own the exit contract for a service: what a clean shutdown must guarantee, how long you are willing to wait for in-flight work, and what you deliberately abandon so shutdown stays bounded.
## The rule When the function `main` returns, the program terminates. The Go specification is blunt about it: program execution finishes when `main` returns, and it **does not wait for other goroutines to complete**. The process exits with status 0. That is the whole rule, but it has sharp consequences worth spelling out. ## What runs and what does not Returning from `main` normally does run `main`'s own deferred calls — the returning function is unwound like any other. What does *not* happen is any unwinding of the other goroutines: - A goroutine parked on a channel receive is simply gone. - A goroutine halfway through writing a file never reaches its `defer f.Close()`. - A goroutine that had buffered output never flushes it. - Nothing panics, nothing is logged, and the exit status is still 0. A related trap is `os.Exit`, which is more brutal still: it terminates the program immediately and does not run **any** deferred calls, not even in the calling goroutine. ## The symptom beginners hit ```go func main() { for _, h := range hosts { go check(h) // each one prints a line } // main returns here } ``` This is a program that usually prints nothing. It is not that the goroutines failed; they were never scheduled before the process died. Add a single blocking operation before the end of `main` and the output appears — which is exactly why sprinkling a sleep at the bottom of `main` is such a common and such a bad fix. A sleep is a guess: too short and you still lose output, too long and every run pays for it. It also hides the real question, which is *who is responsible for these goroutines finishing*. ## Waiting properly The runtime provides no join operation, so waiting is arranged in your own code. The simplest form uses a channel: ```go done := make(chan struct{}) go func() { check(host) close(done) }() <-done // main blocks until the goroutine closes done ``` For several goroutines, `sync.WaitGroup` is the standard counter, and for work that can fail, a channel of results or errors lets `main` both wait and collect. The principle behind all of them is the same: **whoever writes the `go` statement owns knowing when it finished**. ## The main goroutine is not otherwise special Apart from the exit rule, the main goroutine is an ordinary goroutine. It has the same growable stack, it is scheduled the same way, it can block on the same primitives. One curiosity worth knowing: `runtime.Goexit` called from the main goroutine terminates *that goroutine* without `main` returning, so the program keeps running its other goroutines — and crashes once the last one finishes, because there is nothing left to run. ## Why the language chose this An alternative design — wait for every goroutine before exiting — sounds friendlier but is worse. A single leaked goroutine blocked forever on a channel would hang every program at shutdown, and a background worker deliberately looping forever would make clean exit impossible. Go instead makes process lifetime depend on exactly one thing, the main goroutine, and leaves coordination to the program. Shutdown becomes explicit: `main` waits for what must finish, and everything else is genuinely disposable. ## What an interviewer is checking That you know the rule, that you do not reach for `time.Sleep` as a synchronisation tool, and that you can say what you would wait on instead. Being able to describe the failure mode — output silently missing, deferred cleanup silently skipped, exit status still 0 — shows you have actually been bitten by it.
- Do deferred calls inside a still-running goroutine run when main returns?No. Only the deferred calls of the functions unwinding in the main goroutine run. Other goroutines are stopped where they are, so `defer f.Close()` and any buffered output in them are lost. `os.Exit` is harsher still and skips deferred calls everywhere, including in the calling goroutine.
- Why is time.Sleep at the end of main a bad way to let goroutines finish?It is a guess about timing, not a synchronisation mechanism. Too short and you still lose work on a slow machine or under load; too long and every run wastes the difference. Wait on something that actually signals completion — a channel receive, or a `sync.WaitGroup`.
- What happens if the main goroutine calls runtime.Goexit?That goroutine ends without `main` returning, running its deferred calls, and the program keeps executing its other goroutines. When the last of them finishes there is nothing left to run, and the program crashes rather than exiting cleanly.
saying these in an interview costs you the question
- Says the runtime waits for goroutines before exiting
- Uses time.Sleep at the end of main as synchronisation
- Assumes deferred cleanup in every goroutine still runs
- Expects a non-zero exit status when goroutines are cut off