A select timeout that leaks a goroutine

Read the explanation
What if

The timeout wins, but slowCall's goroutine is left stuck on its send, forever.

Predict first

The timeout wins after 50 ms. What happens to slowCall's goroutine?

01234567891011main: go slowCallgoselecttimed outother workmain: goroutines: 22slow network callresult <- "data": stuck

Tap any block to see what that thread is doing.

  • Running
  • Waiting / parked
  • Waiting on I/O
Step 1/5

main starts slowCall on its own goroutine.

1func slowCall(result chan<- string) {2    time.Sleep(200 * time.Millisecond) // a slow network call3    result <- "data"4}5 6func main() {7    result := make(chan string)8    go slowCall(result)9 10    select {11    case r := <-result:12        fmt.Println("got", r)13    case <-time.After(50 * time.Millisecond):14        fmt.Println("timed out")15    }16 17    time.Sleep(300 * time.Millisecond) // main carries on with other work18    fmt.Println("goroutines:", runtime.NumGoroutine())19}

Explanation

What just happened

select waited for whichever came first: the result or time.After(50ms). The timeout won and main moved on. But nothing stopped slowCall. It finished its call and tried to send on result, an unbuffered channel nobody would ever read again. So it waited forever in [chan send].

runtime.NumGoroutine() said 2 long after the timeout. That goroutine leaked.

Try the what-if switch

Why

Go has no way to kill a goroutine. It stops only by returning, so it has to know when to stop. That’s what a context is for.

Watch out

Cancelling a context stops nothing by itself. It only helps code that checks ctx.Done().

Common questions

Does a select timeout stop the goroutine?

No. The timeout only ends the wait in select. The other goroutine keeps running, and if it then sends on an unbuffered channel nobody reads, it's stuck in [chan send] forever.

How do I cancel a goroutine in Go?

You can't stop it from outside. Pass a context.Context and have the goroutine select on ctx.Done(), so it returns when the caller gives up.

What does ctx.Err() return after a timeout?

context deadline exceeded for a context made with context.WithTimeout whose time ran out.

Verified: this timeline matches a real program, checked in CI on Go 1.26 and 1.27. How we verify. Unclear or wrong? Tell us