Golang channel - avoid deadlock

Tung Nguyen
3 min readOct 30, 2020

Deadlock

It is a very common practice that people use channel to receive data from Go routine in order to implement a concurrency solution. Yeah using channel in Go routine is super convenient but if you are not really familiar with it, you may get some unexpected behaviors. Let’s take a look on the following example.

package mainimport (
"fmt"
"time"
"sync"
)
func ConcurrentSum(l int) uint64 {
var wg sync.WaitGroup
c := make(chan uint64)
for i := 0; i < l; i++ {
wg.Add(1)
go func(n int) {
//Suppose here we do a heavy API call instead
c <- uint64(n)
time.Sleep(time.Millisecond)
wg.Done()
}(i)
}
wg.Wait()
close(c)

return processResult(c)
}
func processResult(c <-chan uint64) uint64 {
total := uint64(0)
for n := range c {
total += n
}
return total
}
func main() {
sum := ConcurrentSum(10);
//What is the output here?
fmt.Println("Sum = ", sum)
}

Try this code on Golang playground at https://play.golang.org/p/mn6S87FiwFt

The code is quite simple but if you expect to get an output of “Sum = 45”, then you never get it. Instead you will get “fatal error: all goroutines are asleep — deadlock!”. WHY? Because channel c is an unbuffered channel so sending will be blocked until it is ready to receive but here there is no indicator that channel c is ready.

Resolve deadlock

To fix the above issue simply we just need to wait and close the channel in a Go routine as below.

package mainimport (
"fmt"
"time"
"sync"
)
func ConcurrentSum(l int) uint64 {
var wg sync.WaitGroup
c := make(chan uint64)
for i := 0; i < l; i++ {
wg.Add(1)
go func(n int) {
//Suppose here we do a heavy API call instead
c <- uint64(n)
time.Sleep(time.Millisecond)
wg.Done()
}(i)
}

go func() {
wg.Wait()
close(c)
}()


return processResult(c)
}
func processResult(c <-chan uint64) uint64 {
total := uint64(0)
for n := range c {
total += n
}
return total
}
func main() {
sum := ConcurrentSum(10);
//Output: Sum = 45
fmt.Println("Sum = ", sum)
}

With the small change in bold lines, now you can get an output of “Sum = 45” on Golang playground at https://play.golang.org/p/rb9_g-5TWXn

Make your life simple

However, the thing will be easier if you use a buffered channel, which accepts a limited number of values without any blocking.

package mainimport (
"fmt"
"time"
"sync"
)
func ConcurrentSum(l int) uint64 {
var wg sync.WaitGroup
c := make(chan uint64, l)
for i := 0; i < l; i++ {
wg.Add(1)
go func(n int) {
//Suppose here we do a heavy API call instead
c <- uint64(n)
time.Sleep(time.Millisecond)
wg.Done()
}(i)
}
wg.Wait()
close(c)

return processResult(c)
}
func processResult(c <-chan uint64) uint64 {
total := uint64(0)
for n := range c {
total += n
}
return total
}
func main() {
sum := ConcurrentSum(10);
//Output: Sum = 45
fmt.Println("Sum = ", sum)
}

As you see in the new code, the only change is to define length of channel c and now it becomes a buffered channel. If you try the code on Golang playground, you will get “Output: Sum = 45”. Congratulation. https://play.golang.org/p/KzxQj_bmLeb

Have fun with Golang and enjoy your life.

--

--

Tung Nguyen

A coding lover. Mouse and keyboard are my friends all day long. Computer is a part of my life and coding is my cup of tea.