← blog
#8ConcurrencyB1

Three golden rules

Avoid running a producer and consumer on the same goroutine. Always have a clear exit path.
Use context.Context.

Before all the details, let's review the jargon first:

  • Producer is sender: goroutine that sends data to a channel.
  • Consumer is receiver: goroutine that receives data from a channel.
  • Caller: goroutine that starts the producer and consumer.

The rules are easy to remember and they prevent many concurrency-related bugs, but they are often overlooked or ignored.

#1: One side must already be running.

A producer and its consumer should not run on the same goroutine. Otherwise it can deadlock:

fatal error: all goroutines are asleep - deadlock!

If the caller consumes from a channel, start the producers first. If the caller produces to a channel, start the consumers first. A big enough buffer also works, but it is not a rock solid solution, it works until you add more items than the buffer can hold.

#2: Every goroutine needs a way to stop and exit. You must be 100% clear on this.

Before you type go, decide what makes it return: a closed channel or a cancelled context. Without one, the goroutine leaks: it stays blocked and holds its memory until the program exits.

Examples:

Closed channel (by producer):

  • close(ch) + select { case item, ok := <-ch: if !ok { return } }
  • for item := range ch { ... } return

Cancelled context (by consumer):

  • cancel() + select { case <-ctx.Done(): return }

#3: Context comes first.

Any function that waits (I/O, channel operations, etc.) should take ctx context.Context as its first argument, and return when the context is cancelled.

Once we start discussing worker pools, fan-out, pipelines, semaphores, errgroup, etc., you will see that they all build on these three rules.
These are not the only rules and principles to follow, but they are more than enough to get you started.

And don't forget to run your tests with go test -race ./...