Working with Goroutines is simple. You simply pre-prend your function call with go and you’re off and running.

Here’s an example which calls the expensive() function five times before exiting.

package main

import (
	"fmt"
	"time"
)

func expensive(id int) {

	fmt.Printf("Worker %d starting\n", id)

	time.Sleep(time.Second)  // Oh, so expensive!

	fmt.Printf("Worker %d ending\n", id)
}

func main() {

	for i := 0; i < 5; i++ {

		go expensive(i)
	}

	fmt.Println("Exiting!")
}

But it has a problem… running the program shows output like this:

$ go run main.go
Exiting!
Worker 1 starting
$

Because the calls to expensive() are done ascynchronously via goroutines, those calls to not block and we exit before they can finish their work.

So how can we wait until all the goroutines finish before exiting? Enter WaitGroups!

Here’s the same code, modified to use WaitGroups:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {

    fmt.Printf("Worker %d starting\n", id)

    time.Sleep(time.Second)

    fmt.Printf("Worker %d ending\n", id)

    wg.Done()
}

func main() {

    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {

        wg.Add(1)
        go worker(i, wg)
    }

    fmt.Println("Waiting...")
    wg.Wait()
}

Notice a few things about it:

  • Every time we spin up a new Goroutines, we add 1 to the wg counter with wg.Add(1).
  • Every time the function exits, we call wg.Done() to tell the WaitGroup that the code is done.
  • We pass the WaitGroup variable around as a pointer. This is important!

But what happens if we increment the WaitGroup counter inappropriately by more than 1 by changing wg.Add(1) to wg.Add(2)?

We get a deadlock and Go panics.

$ go run main.go
Waiting...
Worker 1 starting
Worker 3 starting
Worker 4 starting
Worker 2 starting
Worker 0 starting
Worker 0 ending
Worker 3 ending
Worker 1 ending
Worker 2 ending
Worker 4 ending
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x0?)
	/opt/homebrew/Cellar/go/1.18.3/libexec/src/runtime/sema.go:56 +0x2c
sync.(*WaitGroup).Wait(0x14000122010)
	/opt/homebrew/Cellar/go/1.18.3/libexec/src/sync/waitgroup.go:136 +0x88
main.main()
	/Users/james_simas/Desktop/GarbageScripts/go/waitgrouoptest/main.go:31 +0xf0
exit status 2
$