๐ŸŽ“ A1Learner
๐Ÿ”
0 of 28 lessons completed

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?