Go Goroutine Leaks
A goroutine leak happens when a goroutine waits forever with no way to stop. The goroutine keeps its memory and never ends.
A worker that stops on quit
Main sends on the quit channel. The worker prints its line and reports back on the joined channel.
package main
import "fmt"
func worker(quit <-chan struct{}, joined chan<- struct{}) {
<-quit
fmt.Println("Worker stopped")
joined <- struct{}{}
}
func main() {
quit := make(chan struct{})
joined := make(chan struct{})
go worker(quit, joined)
quit <- struct{}{}
<-joined
fmt.Println("Main done")
}The same stop through a context
The worker selects on the Done channel of a context. Main cancels the context, and the worker stops.
package main
import (
"context"
"fmt"
)
func worker(ctx context.Context, joined chan<- struct{}) {
select {
case <-ctx.Done():
fmt.Println("Worker stopped")
joined <- struct{}{}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
joined := make(chan struct{})
go worker(ctx, joined)
cancel()
<-joined
fmt.Println("Main done")
}A generator that can always stop
The generator selects on done while it sends each value. The consumer takes three values and closes done.
package main
import "fmt"
func generator(done <-chan struct{}, out chan<- int, joined chan<- struct{}) {
for i := 1; ; i++ {
select {
case out <- i:
case <-done:
fmt.Println("Generator stopped")
joined <- struct{}{}
return
}
}
}
func main() {
done := make(chan struct{})
out := make(chan int)
joined := make(chan struct{})
go generator(done, out, joined)
for i := 0; i < 3; i++ {
fmt.Println("Got:", <-out)
}
close(done)
<-joined
fmt.Println("Main done")
}Example
package main
import "fmt"
func counter(done <-chan struct{}, out chan<- int, joined chan<- struct{}) {
fmt.Println("Counter started")
for i := 1; ; i++ {
select {
case out <- i:
case <-done:
fmt.Println("Counter stopped")
joined <- struct{}{}
return
}
}
}
func main() {
done := make(chan struct{})
out := make(chan int)
joined := make(chan struct{})
go counter(done, out, joined)
last := 0
for i := 0; i < 3; i++ {
last = <-out
fmt.Println("Count:", last)
}
close(done)
<-joined
fmt.Println("Last count:", last)
}Key points
- A goroutine left waiting forever keeps its memory and never ends.
- A quit channel gives a worker a clear stop signal.
- A worker can wait on the Done channel of a context instead.
- Closing the quit channel or cancelling the context wakes the worker.
- A generator that selects on done while it sends can always stop.
- The command `go run -race` finds data races.
In this example, a counter service sends counting numbers on a channel. Main takes three counts and closes done. The service sees done in its select and prints its stopped line. Main then prints the last count it received.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท Go Playground
Exercise
A worker waits on a channel that no code will ever send on or close. What is true of the worker?