How do you stop a goroutine you started in Go, given the runtime offers no kill call?
answer
- no external kill switch exists
- the stopper arrives as a parameter
- close broadcasts, a send does not
- select on ctx.Done() and return
- a stop request is not a join
basics
~20 sYou 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.
solid answer
~50 sNothing outside a goroutine can end it: a goroutine finishes only when its function returns. So whoever writes the `go` statement has to hand the goroutine a way to be told to stop, and the usual Go delivery mechanisms are a `context.Context` passed as the first parameter or a `done chan struct{}` the starter closes. The goroutine's loop then does `select { case <-ctx.Done(): return; case ... }`, so cancellation is checked at every turn of the loop. Closing a channel is the right broadcast primitive here: `close(done)` unblocks every goroutine receiving from it at once, whereas sending one value would release only one of them. And signalling is only half the contract — the starter usually also needs a way to know the goroutine actually returned, which is a separate channel or a join.
code
go · 12 linesfunc poll(ctx context.Context, every time.Duration, work func()) {
t := time.NewTicker(every)
defer t.Stop()
for {
select {
case <-ctx.Done():
return // the starter asked us to stop
case <-t.C:
work()
}
}
}go deeper
Recall the shape: a goroutine gets its stop signal as a parameter — a context.Context or a done channel — and returns when the signal fires. Be able to write the select with a ctx.Done() case from memory.
Explain why close is used rather than a send (it releases every receiver), why the element type is struct{}, and why a plain bool flag is a data race. Show where the cancellation check goes in a loop that has no select.
Show that you design stopping in at creation time and handle the cases where a signal cannot reach the goroutine: a blocked channel send, a read with no deadline, a long computation. Name the confirmation half of the contract, not just the request.
Own the convention across a codebase: every function that starts a goroutine takes a stop signal and exposes a way to observe exit, and reviewers reject a bare go statement in library code that offers neither.
## Goroutines have no external stop button Go deliberately gives you no goroutine handle: `go f(x)` returns nothing, there is no goroutine id, and there is no `kill` or `interrupt` call you can aim at a running goroutine. A goroutine ends in exactly one way — the function it is running returns (or the whole process dies). `runtime.Goexit` exists, but it terminates *the goroutine that calls it*, so it is not a way for one goroutine to stop another. The consequence is a design rule rather than an API: **whoever writes `go` owns stopping it**, and must build the stopping in at the moment the goroutine is created. ## The two delivery mechanisms ### A context.Context parameter The modern default. The goroutine's function takes `ctx context.Context` as its first parameter and watches `ctx.Done()`, a `<-chan struct{}` that is closed when the context is cancelled or its deadline passes: ```go for { select { case <-ctx.Done(): return ctx.Err() case item := <-in: process(item) } } ``` The starter holds the other end: it created the context (typically with `context.WithCancel`) and calls the returned cancel function when it wants the goroutine to wind down. Because contexts form a tree, cancelling a parent cancels every context derived from it, so one call can stop a whole fan-out of goroutines that were each handed a derived context. ### A done chan struct{} The older, dependency-free form, still the right choice inside a self-contained type that has no request-scoped context to carry. The starter creates `done := make(chan struct{})`, passes it in, and later calls `close(done)`. Two details matter. First, the element type is `struct{}` because the channel carries no data — it exists purely as a signal, and `struct{}` occupies no space. Second, you *close* it rather than send on it. A receive from a closed channel returns immediately with the zero value, forever and for every receiver, so a single `close(done)` releases all N goroutines waiting on it. Sending one value would wake exactly one goroutine, which is the classic bug in hand-rolled shutdown code. ## Cancellation is cooperative, and only reaches code that looks Neither mechanism preempts anything. `close(done)` and cancelling a context just make a channel readable; the goroutine stops only when its own code notices and returns. That has practical consequences: - A goroutine in a tight computation loop with no select never stops. You need an explicit check, e.g. `if ctx.Err() != nil { return }` at the top of each iteration. - A goroutine parked in a blocking call that does not know about your signal — a channel send nobody reads, a `Read` on a connection with no deadline, a mutex held elsewhere — will not wake up either. For network and file I/O the Go-specific answers are the context-aware stdlib entry points, or `SetReadDeadline`/`SetWriteDeadline` on the connection, or closing the connection so the blocked call returns an error. ## Do not use a plain bool flag A `var stop bool` written by one goroutine and read by another is a data race: the race detector will flag it, and without synchronisation there is no guarantee the reader ever observes the write. Use a channel or a context; if you truly want a flag, it has to be `sync/atomic` — but a closed channel is better because it also lets the goroutine *block* on the signal inside a `select` instead of polling it. ## Signalling is not joining The last piece a first answer usually misses: telling a goroutine to stop tells you nothing about *when* it stopped. Immediately after cancelling, the goroutine may still be mid-iteration, still writing to a file, still sending on a channel. If the code after cancellation tears down anything the goroutine touches — closes a file, drops a database handle, ends a test — the starter needs a second signal for "it has returned": a `stopped` channel the goroutine closes with `defer`, or a `sync.WaitGroup` (or an `errgroup`) it waits on. Owning a goroutine means owning both halves: the stop request and the confirmation.
- When would you pass a done chan struct{} instead of a context.Context?When the goroutine's lifetime is tied to an object rather than to a request or a deadline — a background loop inside a type whose `Close` stops it. A `done` channel keeps the type's API free of a context parameter it would only ever use as an on/off switch. Prefer a context whenever the work is request-scoped, has a deadline, or must propagate cancellation into other context-aware calls.
- Why chan struct{} rather than chan bool for a done channel?Because no value is ever sent — the signal is the close, not the payload. `struct{}` is zero-width, so the channel carries nothing, and the type itself documents that receiving a value is not the protocol. A `chan bool` invites someone to send `true`, which wakes exactly one waiter and leaves the rest blocked.
- Your goroutine is blocked in a network read when you cancel. What happens?Nothing, until the read returns. Cancellation only reaches code that checks it, and a blocked syscall is not checking. Use an API that takes the context, or set a deadline on the connection with `SetReadDeadline` so the read fails and the loop reaches its `ctx.Done()` case, or close the connection from the owner so the pending read errors out.
You cannot yank a running goroutine off the road; you can only agree on a whistle before it leaves and trust it to pull over when it hears one.
saying these in an interview costs you the question
- Claims you can kill or interrupt another goroutine from outside
- Uses a shared bool flag with no synchronisation as the stop signal
- Sends one value on the done channel instead of closing it
- Thinks cancelling a context forcibly unwinds the goroutine
- Assumes the goroutine has finished the instant cancel returns