skip to content

Goroutine Lifecycle

What a goroutine actually is — a few kilobytes of growable stack multiplexed onto OS threads by the Go runtime — and how they start, finish, and go wrong. Interviewers open here to see whether you treat goroutines as free or understand what the scheduler is doing on your behalf.

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

explore

questions

17

In Go, what does the go keyword do when placed in front of a function call?

level: juniorimportance: must knowfreq 88%

answer

  1. the caller does not wait
  2. nothing at all comes back to you
  3. a statement, not an expression
  4. results have to travel over a channel

basics

~20 s

It runs that call in a new goroutine and returns immediately, so the caller carries on without waiting. It is a statement, not an expression: it produces no value, no handle and no id you can hold on to.

solid answer

~50 s

`go f(x)` starts `f(x)` as an independent goroutine and the calling goroutine continues with the next statement straight away. The function value and the arguments are evaluated first, in the calling goroutine; only the call itself is handed to the new goroutine, which the runtime schedules onto an OS thread for you. The operand must be a function or method call, so `go f` alone does not compile, and because `go` is a statement you cannot write `v := go f()` — anything `f` returns is discarded, so results have to come back over a channel you set up yourself. You also get nothing back to wait on or cancel with: no goroutine handle, no id, no kill API. A new goroutine costs about 2 KB of growable stack, which is why starting thousands is normal Go.

code

go · 6 lines
go
go check(host)              // ok: a function call
go conn.ping()              // ok: a method call
go func() { check(host) }() // ok: a call of a function literal

// go check                 // invalid: not a call
// v := go check(host)      // invalid: go is a statement, it has no value

go deeper

for a junior

Be ready to say in one sentence that go starts the call concurrently and the caller keeps going, and that you cannot capture a return value from it. Knowing that a channel is how results come back is enough here.

for a middle

Explain what the operand may be, that the function value and arguments are evaluated in the calling goroutine, and that the runtime multiplexes goroutines onto OS threads rather than creating one thread per go statement.

for a senior

Show that you treat the missing pieces as design work: no handle, no id, no kill means the code that writes go owns arranging results, shutdown and waiting. Interviewers listen for whether you notice that responsibility.

for a principal

Frame it as a language tradeoff: the go statement is deliberately minimal so lifecycle policy lives in library code rather than the runtime. Be able to say what conventions you would standardise on so that cheapness does not become sprawl.

## What the statement is A **goroutine** is a function executing concurrently with other goroutines in the same address space. The `go` statement is the only way to create one: ```go go check(host) ``` That single keyword turns an ordinary call into a concurrent one. The calling goroutine does not block: it proceeds to the statement after the `go` statement while `check(host)` runs somewhere else, possibly on another CPU, possibly interleaved on the same one. Every Go program already has one goroutine before it ever runs a `go` statement — the **main goroutine**, created by the runtime to run package initialisation and then `main`. Everything else descends from a `go` statement somebody wrote. ## What the operand may be The operand of `go` must be a **function or method call**. These are all legal: ```go go check(host) // ordinary function call go conn.ping() // method call go func() { check(host) }() // call of a function literal ``` These are not: - `go check` — an identifier, not a call. - `go x + 1` — an expression, not a call. - `v := go check(host)` — `go` is a statement, so it has no value to assign. That last point is the one newcomers trip on. `go` gives you nothing back. Whatever the function returns is thrown away — `go vet` will not even complain, because discarding a return value is legal Go. If the work produces a result, the goroutine has to send it somewhere: a channel, or a variable protected by a mutex. ## What it does *not* give you Compared with a thread API in most other languages, the `go` statement is deliberately bare: - **No handle.** There is no `Goroutine` object, no future, no join method. The `go` statement returns nothing at all. - **No id.** The runtime numbers goroutines internally and prints those numbers in panic dumps and profiles, but there is no supported API to read one, precisely so nobody builds goroutine-local storage on it. - **No kill.** Nothing can stop a goroutine from the outside. A goroutine ends when its function returns (or when the process exits). Stopping one early requires that it cooperate — it must be *asked*, on a channel or through a cancellable value it is watching. - **No join.** If the code that started a goroutine needs to know when it finished, it must arrange that itself: receive from a channel the goroutine closes or sends on, or use `sync.WaitGroup`. The runtime will not wait for anyone. A useful way to phrase it in an interview: `go` is cheap to write and cheap to run, and every bit of the lifecycle management that other languages hand you is left to you on purpose. ## What it costs A goroutine is not an OS thread. Starting one allocates a small runtime structure and a stack of roughly **2 KB**, which grows and shrinks on demand as the goroutine calls deeper. An OS thread typically reserves megabytes up front. The Go runtime multiplexes many goroutines onto a much smaller set of OS threads, so tens or hundreds of thousands of goroutines in one process is an ordinary design, not a stunt. Blocking a goroutine — on a channel, a mutex or network I/O — parks it and frees its thread for other goroutines rather than pinning a kernel thread. ## Evaluation order, in one line The function value and the parameters are evaluated **in the calling goroutine, at the `go` statement**; only the invocation happens in the new goroutine. So `go check(host)` snapshots `host` right there, while `go func() { check(host) }()` reads `host` later, whenever the goroutine actually runs. ## Lifetime A goroutine's lifetime is independent of the one that started it — with one blunt exception: when `main` returns, the process exits and every other goroutine dies where it stands, deferred calls unrun. Concurrency in Go is easy to start and entirely your responsibility to finish.

  • How does a value computed inside a goroutine get back to the code that started it?
    Over a channel, or into shared state guarded by a mutex. The `go` statement itself discards the call's return values, so the goroutine has to send the result somewhere the starter is reading. The usual shape is a channel created before the `go` statement and received from after it.
  • How many goroutines does a Go program have before it executes any go statement?
    One — the main goroutine. The runtime creates it to run package-level initialisation and then `main`. Every other goroutine in the process exists because some code ran a `go` statement.
  • Is a goroutine an OS thread?
    No. It is a runtime-managed unit of execution with its own small growable stack, multiplexed by the Go scheduler onto a pool of OS threads. Many goroutines share one thread, and a goroutine that blocks on a channel or on network I/O is parked without holding a thread.

saying these in an interview costs you the question

  • Says go f() returns a handle you can join on
  • Thinks each go statement starts a new OS thread
  • Expects to assign the goroutine's return value
  • Believes the runtime can kill a goroutine on request
open as a page

What is a goroutine leak in Go, and why does the runtime never reclaim a goroutine that is blocked forever?

level: juniorimportance: must knowfreq 72%

basics

~20 s

A goroutine leak is a goroutine that never returns because it is blocked forever on an operation nobody will ever complete. The runtime cannot reclaim it, so its stack and everything it references stay alive until the process exits.

open as a page

How do you stop a goroutine you started in Go, given the runtime offers no kill call?

level: juniorimportance: must knowfreq 78%

basics

~20 s

You cannot stop it from the outside. Pass the goroutine a stop signal it watches itself, either a context.Context or a done channel of type chan struct{}, and write the goroutine so it returns when that signal fires.

open as a page

What happens to a Go program when a goroutine panics and nothing recovers it?

level: juniorimportance: must knowfreq 75%

basics

~20 s

The whole process dies, not just that goroutine. The runtime prints the panic value and a stack trace and exits with status 2. Other goroutines are killed where they stand and their deferred functions never run.

open as a page

In Go, when are the function value and arguments of go f(x) evaluated?

level: middleimportance: must knowfreq 68%

basics

~20 s

Immediately, in the calling goroutine, at the go statement itself; only the call runs in the new goroutine. So go f(x) snapshots x on the spot, while go func(){ f(x) }() reads x later, whenever the goroutine happens to run.

open as a page

Why does a goroutine that sends its result on an unbuffered channel leak when the caller stops waiting?

level: middleimportance: must knowfreq 62%

basics

~20 s

A send on an unbuffered channel completes only when a receiver is ready. Once the caller has returned on a timeout, no receiver ever arrives, so the sending goroutine parks on that line forever, holding its result.

open as a page

Why can't a deferred recover in a parent function catch a panic in a goroutine it started?

level: middleimportance: must knowfreq 62%

basics

~20 s

Each goroutine has its own stack and defer chain, and a panic unwinds only that one. A recover sees a panic only on the goroutine running it, so the parent's recover returns nil while the process dies.

open as a page

What happens to still-running goroutines when a Go program's main function returns?

level: juniorimportance: should knowfreq 70%

basics

~20 s

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

open as a page

How much stack does a new goroutine start with, and what happens when it needs more?

level: middleimportance: should knowfreq 55%

basics

~20 s

About 2 KB. When a call would overflow it, the Go runtime allocates a larger stack, copies the existing frames across and adjusts pointers into them, so stacks grow on demand instead of being reserved up front like a thread's.

open as a page

A goroutine ranges over a channel the producer never closes — what happens to that goroutine?

level: middleimportance: should knowfreq 55%

basics

~10 s

A range over a channel ends only when the channel is closed, so the consumer parks on the receive forever. An empty channel, or a producer that has returned, does not end the loop.

open as a page

What must a Go type's Close do so callers know its background goroutine has exited?

level: middleimportance: should knowfreq 58%

basics

~20 s

Close must signal and then join. It cancels the context or closes the done channel the goroutine watches, then blocks until the goroutine confirms it returned, usually by receiving from a channel the goroutine closes in a defer.

open as a page

A Go queue worker's memory climbs for days while message throughput is flat — how do you tell a goroutine leak from a heap leak?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Compare two trends: runtime.NumGoroutine and in-use heap bytes. A goroutine count that ratchets up and never falls back when the queue drains means goroutines are leaking, dragging their captured data with them.

open as a page

Why can a Go library's Close deadlock when its background goroutine is blocked on a channel send?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Because Close waits for a goroutine that is parked in a channel send nobody will receive. The goroutine never reaches its cancellation check, so both sides wait forever. Make the send itself cancellable with a select.

open as a page

How do you stop one subscriber's panicking callback from killing a webhook dispatcher process?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Run each callback in a goroutine whose body begins with a deferred recover, so the panic is contained there and becomes a value. Send that value back on a results channel, attributed to the subscriber.

open as a page

Go gives you no goroutine id and no goroutine-local storage, so how do you tag concurrent work with a request?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

You pass the identifier explicitly. Go hides goroutine ids so nothing can build thread-local storage on them; a run or request id travels as a function argument, as a field on a value the goroutine owns, or inside a context.Context.

open as a page

As a library maintainer, how do you decide what Close must guarantee about goroutines your Go package started?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Decide from what callers do next. If they may release resources, end a test, or count goroutines after Close, then Close must block until every goroutine the package started has returned, and say so in the doc comment.

open as a page

Should every goroutine in your Go service defer a recover, or should panics be allowed to crash it?

level: principalimportance: nice to knowfreq 32%

basics

~20 s

Neither extreme. Recover at trust boundaries where a goroutine runs code you do not own on one independent unit of work; let panics crash the process in your own core paths, because continuing on broken invariants is worse than a restart.

open as a page