Track down race conditions with Go

Classic Computer Science

The classical method for slowing down racers relies on what are known as mutex (mutually exclusive) constructs. Go's sync package provides mutex elements that use Lock() to create a roadblock for subsequent goroutines and clear it again with Unlock() after the critical region has been navigated.

Listing 5 shows the implementation of the book() airplane-seat booking function; the call to the function in Line 16 is surrounded by a pair of mutexes. Before the call, line 15 uses mutex.Lock() to block the section for subsequent goroutines. After the job is done, line 17 uses mutex.Unlock() to lift the block again.

Listing 5


01 package main
02 import (
03   "fmt"
04   "sync"
05   "time"
06 )
08 func main() {
09   seats := 1
10   mutex := &sync.Mutex{}
12   for i := 0; i < 2; i++ {
13     go func(id int) {
14       time.Sleep(100 * time.Millisecond)
15       mutex.Lock()
16       book(id, &seats)
17       mutex.Unlock()
18     }(i)
19   }
21   time.Sleep(1 * time.Second)
22   fmt.Println("")
23 }
25 func book(id int, seats *int) {
26   if *seats > 0 {
27     fmt.Printf("%d booked!\n", id)
28     *seats = 0
29   } else {
30     fmt.Printf("%d missed out.\n", id)
31   }
32 }

For the sake of clarity, the book() function now encapsulates the posting process starting in line 25. As its input, it expects the ID of the goroutine and a pointer to the seats variable, whose value it adjusts accordingly in case of a successful booking.

Height-Adjustable Barrier

One alternative to the mutex construct is provided by the sync.WaitGroup construct, also from the sync package of the Go core library. Its Add() function sets up a block with an adjustable height. A subsequent Wait() waits for the block to be lifted, which a call to the Done() function is happy to do.

The smart thing here is that multiple program parts can set up any number of blocks through subsequent calls to Add(). You then need the same number of calls to Done() before Wait() grants passage again.

The code in Listing 6 waits for this to happen before starting the critical section with a prophylactic Wait() statement in line 15, but the code continues immediately because there are no blocks in the way in the initial state. Line 16 then calls Add(1) to add a barrier just before booking. Afterwards, book() in line 17 can calmly make the booking, before line 18 uses Done() to take down the barrier again.

Listing 6


01 package main
02 import (
03   "fmt"
04   "sync"
05   "time"
06 )
08 func main() {
09   seats := 1
10   wg := &sync.WaitGroup{}
12   for i := 0; i < 2; i++ {
13     go func(id int) {
14       time.Sleep(100 * time.Millisecond)
15       wg.Wait()
16       wg.Add(1)
17       book(id, &seats)
18       wg.Done()
19     }(i)
20   }
22   time.Sleep(1 * time.Second)
23   fmt.Println("")
24 }
26 func book(id int, seats *int) {
27   if *seats > 0 {
28     fmt.Printf("%d booked!\n", id)
29     *seats = 0
30   } else {
31     fmt.Printf("%d missed out.\n", id)
32   }
33 }


As always in Go, there are several possible solutions to the challenges of controlling racing goroutines – after all, these are well-known programming problems.

If two related instructions such as checking and placing a shared variable cannot be done atomically, you need to use a synchronization tool to temporarily block the critical time window between the instructions – to keep out any program threads attempting to rush in.

And always remember to clear the block later on, even if the booking fails for some reason. Otherwise, you have built a permanent lock into your program, which might consume resources or block access to some resources entirely. Test every case to make sure!


  1. Go: "Programming Snapshot – Golang" by Mike Schilli, Linux Magazine issue 250, September 2021, p. 54-60
  2. Listings for this article:

The Author

Mike Schilli works as a software engineer in the San Francisco Bay area, California. Each month in his column, which has been running since 1997, he researches practical applications of various programming languages. If you email him at, he will gladly answer any questions.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Simultaneous Runners

    In the Go language, program parts that run simultaneously synchronize and communicate natively via channels. Mike Schilli whips up a parallel web fetcher to demonstrate the concept.

  • Let's Go!

    Released back in 2012, Go flew under the radar for a long time until showcase projects such as Docker pushed its popularity. Today, Go has become the language of choice of many system programmers.

  • Fighting Chaos

    When functions generate legions of goroutines to do subtasks, the main program needs to keep track and retain control of ongoing activity. To do this, Mike Schilli recommends using a Context construct.

  • Motion Sensor

    Inotify lets applications subscribe to change notifications in the filesystem. Mike Schilli uses the cross-platform fsnotify library to instruct a Go program to detect what's happening.

  • Progress by Installments

    Desktop applications, websites, and even command-line tools routinely display progress bars to keep impatient users patient during time-consuming actions. Mike Schilli shows several programming approaches for handwritten tools.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More