Kodewick
Start learning →
Lesson 23 of 24Contents

Deadlocks and pitfalls

Lesson 23 of 24 · Part 5 · Concurrency · about 15 minutes

When every goroutine has stopped to wait, and nobody can give them what they're waiting for, the program can never move again. That's a deadlock. When Go sees that every goroutine is waiting, it stops the program and shows:

fatal error: all goroutines are asleep - deadlock!

This lesson runs the most common deadlocks one by one. Press Step on any of them, or press "See how it went wrong": each goroutine gets its own lane, and you can see where each one stopped and which channel or lock it's waiting on.

A send with no receiver

prog.go
1package main2 3import "fmt"4 5func main() {6	orders := make(chan string)7	orders <- "order 1"8	fmt.Println(<-orders)9}
Output
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main(...)
	/tmp/sandbox/prog.go:7 +0x1d
exit status 2

What happened: Every goroutine is waiting (on a channel, a lock or a WaitGroup) and none can go on: a deadlock.

On an unbuffered channel, a send waits until someone receives (lesson 19). The receive is on the next line, but main is stuck on the send on line 7 and never gets there. main is the only goroutine, and it's waiting, so nothing can move.

Under the error, goroutine 1 [chan send]: says goroutine 1 (that's main) stopped while sending on a channel. There are two fixes: let another goroutine do the sending (or the receiving), or give the channel a buffer, make(chan string, 1) (lesson 20).

Forgetting close

for v := range ch keeps receiving until the channel is closed. If the sender finishes without close, the range waits for the next value forever:

prog.go
1package main2 3import "fmt"4 5func main() {6	prices := make(chan int)7	go func() {8		for _, p := range []int{30, 45, 25} {9			prices <- p10		}11	}()12	total := 013	for p := range prices {14		total += p15		fmt.Println("got", p)16	}17	fmt.Println("total", total)18}
Output
got 30
got 45
got 25
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main(...)
	/tmp/sandbox/prog.go:13 +0x1d
exit status 2

What happened: Every goroutine is waiting (on a channel, a lock or a WaitGroup) and none can go on: a deadlock.

All three prices arrive, then a deadlock at the range on line 13. Press Step to the end: the lane sending prices has finished, and only main is left, waiting on prices for a value that will never come.

The sender closes the channel when it's done. The safest way is defer close(prices) at the top of the goroutine:

prog.go
1package main2 3import "fmt"4 5func main() {6	prices := make(chan int)7	go func() {8		defer close(prices)9		for _, p := range []int{30, 45, 25} {10			prices <- p11		}12	}()13	total := 014	for p := range prices {15		total += p16	}17	fmt.Println("total", total)18}
Output
total 100

Remember: whoever sends, closes. The receiver shouldn't close a channel, because it can't know whether the sender has more to send.

Forgetting Done

wg.Wait() waits for the counter to get back to 0. If one goroutine never calls Done, the counter never gets there:

prog.go
1package main2 3import (4	"fmt"5	"sync"6)7 8func main() {9	sizes := []int{1200, 0, 800}10	thumbs := make([]int, len(sizes))11	var wg sync.WaitGroup12	for i, s := range sizes {13		wg.Add(1)14		go func() {15			if s == 0 {16				return17			}18			thumbs[i] = s / 1019			wg.Done()20		}()21	}22	wg.Wait()23	fmt.Println(thumbs)24}
Output
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [sync.WaitGroup.Wait]:
main.main(...)
	/tmp/sandbox/prog.go:22 +0x1d
exit status 2

What happened: Every goroutine is waiting (on a channel, a lock or a WaitGroup) and none can go on: a deadlock.

The image of size 0 returns early and skips wg.Done(). The counter stays at 1, and main stops at wg.Wait() on line 22. That's why the last lesson said: put defer wg.Done() on the goroutine's first line, so it runs however the goroutine ends.

Another way to miss a Done: passing a WaitGroup to a function as a copy, not a pointer.

prog.go
1package main2 3import (4	"fmt"5	"sync"6)7 8func check(order string, wg sync.WaitGroup) {9	defer wg.Done()10	fmt.Println("checking", order)11}12 13func main() {14	var wg sync.WaitGroup15	wg.Add(1)16	go check("order 7", wg)17	wg.Wait()18	fmt.Println("all checked")19}
Output
checking order 7
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [sync.WaitGroup.Wait]:
main.main(...)
	/tmp/sandbox/prog.go:17 +0x1d
exit status 2

What happened: Every goroutine is waiting (on a channel, a lock or a WaitGroup) and none can go on: a deadlock.

check finished and did call Done, but on the copy's counter. The wg in main is still at 1. The parameter should be wg *sync.WaitGroup, and the call should pass &wg. go vet warns about this (passes lock by value: a WaitGroup must not be copied).

Locking the same lock twice

prog.go
1package main2 3import (4	"fmt"5	"sync"6)7 8type Cart struct {9	mu    sync.Mutex10	items []int11}12 13func (c *Cart) Total() int {14	c.mu.Lock()15	defer c.mu.Unlock()16	sum := 017	for _, p := range c.items {18		sum += p19	}20	return sum21}22 23func (c *Cart) Add(p int) {24	c.mu.Lock()25	defer c.mu.Unlock()26	c.items = append(c.items, p)27	fmt.Println("added", p, "total", c.Total())28}29 30func main() {31	c := &Cart{}32	c.Add(20)33}
Output
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [sync.Mutex.Lock]:
main.(*Cart).Total(...)
	/tmp/sandbox/prog.go:14 +0x1d
main.(*Cart).Add(...)
	/tmp/sandbox/prog.go:27 +0x1d
main.main(...)
	/tmp/sandbox/prog.go:32 +0x1d
exit status 2

What happened: Every goroutine is waiting (on a channel, a lock or a WaitGroup) and none can go on: a deadlock.

Add holds the lock and calls Total, which tries to Lock the same lock. The lock belongs to Add, so Total waits for Add to unlock, and Add is waiting for Total to return. It waits for itself, on line 14. Go's Mutex doesn't remember who locked it: the same goroutine locking again has to wait like anyone else.

The fix: put the calculation in a lowercase helper method that doesn't lock (say, total()), and have Total and Add each call it once they hold the lock.

Guess first

What happens when this program runs?

1package main2 3import "fmt"4 5func main() {6	ch := make(chan int, 2)7	ch <- 18	ch <- 29	ch <- 310	fmt.Println(<-ch, <-ch, <-ch)11}

One Done or Unlock too many

The other way around: more Dones than Adds makes the counter negative, and the program panics at once:

prog.go
1package main2 3import (4	"fmt"5	"sync"6)7 8func main() {9	orders := []string{"order 1", "order 2", "order 3"}10	var wg sync.WaitGroup11	wg.Add(2)12	for _, o := range orders {13		fmt.Println("handling", o)14		wg.Done()15	}16	wg.Wait()17}
Output
handling order 1
handling order 2
handling order 3
panic: sync: negative WaitGroup counter

goroutine 1 [running]:
main.main(...)
	/tmp/sandbox/prog.go:14 +0x1d
exit status 2

What happened: Done was called more times than Add.

Three orders, but only Add(2). The third Done takes the counter to -1: sync: negative WaitGroup counter. Use wg.Add(len(orders)), or wg.Add(1) right before each go, and the count can't be off.

Locks are the same: Unlock a Mutex that isn't locked and the program stops with fatal error: sync: unlock of unlocked mutex. A common cause is writing defer mu.Unlock() and then also calling Unlock by hand.

Stuck without an error

Go only reports a deadlock when every goroutine is waiting. If main can still finish, a stuck goroutine doesn't cause an error. It just waits there, quietly, forever:

1func main() {2	results := make(chan int)3	go func() {4		results <- 42 // nobody receives: this goroutine waits here forever5	}()6	fmt.Println("leaving without the result")7}

This program ends normally, with no error. In a short program that's harmless; in a server that runs for months, stuck goroutines pile up until they fill the memory. This is a goroutine leak. Every time you start a goroutine, ask yourself: how does it end?

A checklist

When you see all goroutines are asleep, first find the line where main stopped, then press Step to see what the other lanes are waiting for:

main stopped atCommon causeFix
ch <- vNobody receivesReceive in another goroutine, or add a buffer
for v := range chThe sender never closed itclose(ch) when done, best as defer close(ch)
wg.Wait()A goroutine missed Done, or the WaitGroup was copieddefer wg.Done(), pass &wg
mu.Lock()The lock was never given back, or locked twice by one goroutinedefer mu.Unlock(), don't call locking code while holding the lock

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it. All three programs deadlock right now.

Exercise 1

Fix it: prices nobody receives

sum tries to send every price into the channel first and only then receive them to add up, but the very first send gets stuck. Make the program show the totals of both price lists. (You don't need to change main.)

It should print
100
20
exercise1.go
1package main2 3import "fmt"4 5func sum(prices []int) int {6	ch := make(chan int)7	for _, p := range prices {8		ch <- p9	}10	total := 011	for range prices {12		total += <-ch13	}14	return total15}16 17func main() {18	fmt.Println(sum([]int{30, 45, 25}))19	fmt.Println(sum([]int{12, 8}))20}
Output
Press Run or Ctrl+Enter
Exercise 2

Fix it: canceled orders that keep Wait waiting

Each order gets a goroutine that works out what to pay (quantity × unit price); orders with quantity 0 were canceled and are skipped. But the program gets stuck at wg.Wait(). Make it show the amount for each order.

It should print
[30 0 75 0 15]
exercise2.go
1package main2 3import (4	"fmt"5	"sync"6)7 8func main() {9	qty := []int{2, 0, 5, 0, 1}10	price := 1511	amounts := make([]int, len(qty))12	var wg sync.WaitGroup13	for i, q := range qty {14		wg.Add(1)15		go func() {16			if q == 0 {17				return18			}19			amounts[i] = q * price20			wg.Done()21		}()22	}23	wg.Wait()24	fmt.Println(amounts)25}
Output
Press Run or Ctrl+Enter
Exercise 3

Fix it: a cart that locks inside its lock

Add holds the lock and calls Total, which locks the same lock again. Add a method total() that adds up the cart without locking, and have both Total and Add use it. The program puts three things in the cart.

It should print
added 20 total 20
added 35 total 55
added 5 total 60
final 60
exercise3.go
1package main2 3import (4	"fmt"5	"sync"6)7 8type Cart struct {9	mu    sync.Mutex10	items []int11}12 13func (c *Cart) Total() int {14	c.mu.Lock()15	defer c.mu.Unlock()16	sum := 017	for _, p := range c.items {18		sum += p19	}20	return sum21}22 23func (c *Cart) Add(p int) {24	c.mu.Lock()25	defer c.mu.Unlock()26	c.items = append(c.items, p)27	fmt.Println("added", p, "total", c.Total())28}29 30func main() {31	c := &Cart{}32	c.Add(20)33	c.Add(35)34	c.Add(5)35	fmt.Println("final", c.Total())36}
Output
Press Run or Ctrl+Enter

What you learned

  • When every goroutine is waiting and none can move, that's a deadlock: fatal error: all goroutines are asleep - deadlock!. The error shows the line where main stopped; Step shows what each lane is waiting for.
  • A send on an unbuffered channel needs another goroutine to receive.
  • To range over a channel, the sender must close it when done: whoever sends, closes.
  • Put defer wg.Done() on a goroutine's first line; pass a WaitGroup as a pointer.
  • Go's Mutex can't be locked twice, not even by the same goroutine; put the work you need inside the lock in a helper that doesn't lock.
  • More Dones than Adds is a panic; unlocking an unlocked lock is a fatal error.
  • When only some goroutines are stuck, there's no error: they leak quietly. Make sure every goroutine has a way to end.
Finished this lesson?Mark it. Progress is saved in this browser, and you can undo it.