Goroutine Management

Goroutines are lightweight, but they are not free: at the very least, they consume memory for stack and CPU scheduling. Although these costs are small for the use of Goroutines, generating them in large quantities without a controlled lifecycle can lead to serious performance issues. Goroutines with unmanaged lifecycles may also lead to other problems, such as preventing unused objects from being garbage collected and retaining unused resources.

Therefore, do not leak goroutines in your code. Use go.uber.org/goleak to test for goroutine leaks within a package that may produce goroutines.

In general, each goroutine must:

  • Have a predictable stopping time; or
  • Have a way to signal the goroutine that it should stop.

In both cases, there must be a way for the code to block and wait for the goroutine to complete.

For example:

Not recommended:

go func() {
  for {
    flush()
    time.Sleep(delay)
  }
}()
// There is no way to stop this goroutine. It will run indefinitely until the application exits.

Recommended:

var (
  stop = make(chan struct{}) // Signal the goroutine to stop
  done = make(chan struct{}) // Signal that the goroutine has exited
)
go func() {
  defer close(done)
  ticker := time.NewTicker(delay)
  defer ticker.Stop()
  for {
    select {
    case <-ticker.C:
      flush()
    case <-stop:
      return
    }
  }
}()

// ... other code
close(stop)  // Indicate that the goroutine should stop
<-done       // and wait for it to exit

// This goroutine can be waited for with close(stop), and we can wait for it to exit <-done.

Waiting for Goroutines to Exit

When given a goroutine generated by the system, there must be a way to wait for the goroutine to exit. There are two common methods to achieve this:

  • Using sync.WaitGroup. Use this method when waiting for multiple goroutines.
var wg sync.WaitGroup
for i := 0; i < num; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        // ...
    }()
}
wg.Wait()
  • Adding another chan struct{}, and closing it after the goroutine is completed. Use this method if there is only one goroutine.
done := make(chan struct{})
go func() {
    defer close(done)
    // ...
}()
// To wait for the goroutine to finish:
<-done

The init() function should not create goroutines. Also, refer to Avoid using init().

If a package requires a background goroutine, it must expose an object responsible for managing the goroutine’s lifecycle. This object must provide a method (Close, Stop, Shutdown, etc.) to indicate the termination of the background goroutine and wait for its exit.

Not Recommended Approach:

func init() {
    go doWork()
}
func doWork() {
    for {
        // ...
    }
}
// When the user exports this package, a background goroutine is unconditionally generated. Users cannot control the goroutine or stop it.

Recommended Approach:

type Worker struct{ /* ... */ }
func NewWorker(...) *Worker {
    w := &Worker{
        stop: make(chan struct{}),
        done: make(chan struct{}),
        // ...
    }
    go w.doWork()
}
func (w *Worker) doWork() {
    defer close(w.done)
    for {
        // ...
        select {
        case <-w.stop:
            return
        }
    }
}
// Shutdown tells the worker to stop
// and waits for it to complete.
func (w *Worker) Shutdown() {
    close(w.stop)
    <-w.done
}

Generate the worker only when requested by the user, and provide a method to shut down the worker so that the user can release the resources used by the worker.

Note that if the worker manages multiple goroutines, a WaitGroup should be used.