Francesc Campoy
20 Apr 2018
ā¢
6 min read
This blog post is complementary to episode 26 of justforfunc which you can watch right below.
Everybody that has written some Go knows about channels. Most of us also know that the default value for channels is nil. But not many of us know that this nil value is actually useful.
I got this same question on twitter, from a developer learning Go, wondering whether Go nil channels existed just for completeness.
It does makes sense to wonder whether theyāre useful, as their behavior seems to indicate otherwise.
Given a nil
channel c
:
<-c
receiving from c
blocks foreverc <- v
sending into c
blocks foreverclose(c)
closing c
panicsBut I still insist they are useful. Let me introduce a problem whose solution seems obvious at first, but it is actually not as easy as one might think and actually benefits from nil channels.
Your mission, should you choose to accept it, is to write a function that given two channels a
and b
of some type returns one channel c
of the same type. Every element received in a
or b
will be sent to c
, and once both a
and b
are closed c
will be closed too.
Before we start, letās write a function that will help us test our solution. This function returns a channel that will eventually receive, at random intervals, all of the given values and finish by being closed.
func asChan(vs ...int) <-chan int {
c := make(chan int)
go func() {
for _, v := range vs {
c <- v
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
}
close(c)
}()
return c
}
This function creates a channel, starts a new go routine that sends values to the created channel, and finally returns the channel.
This is pretty common pattern when dealing with channels, so make sure you understand how it works before you continue reading.
Since we donāt really have a preference over a
or b
weāre going to avoid creating a preference by choosing on which channel we should range
first. Letās instead keep the symmetry and use an infinite loop and select
over both channels.
func merge(a, b <-chan int) <-chan int {
c := make(chan int)
go func() {
for {
select {
case v := <-a:
c <- v
case v := <-b:
c <- v
}
}
}()
return c
}
This looks pretty good, letās write a quick test and run it.
func main() {
a := asChan(1, 3, 5, 7)
b := asChan(2, 4, 6, 8)
c := merge(a, b)
for v := range c {
fmt.Println(v)
}
}
This should print 1 to 8 in some order and end successfully. Letās see what happens.
> go run main.go
1
2
3
4
5
6
7
8
0
0
0
0
0
0
0
š±
Ok, so clearly this is not good because the program doesnāt ever finish. Once it has printed the values from 1 to 8 it starts printing zeros forever.
What happens when we receive from a closed channel? We get the default value of the type of the channel. In our case, the type is int
so the value is 0
.
We could check for channels being closed by comparing to zero, but what if one of the values we received was a zero? Instead we can use the āvalue comma okā syntax:
v, ok := <- c
When using this syntax ok
is a boolean that will be true
for as long the channel is open. Knowing this we can avoid sending superfluous zeros into c
.
We should also stop iterating at some point ā¦ so letās keep track of when both channels are closed too.
func merge(a, b <-chan int) <-chan int {
c := make(chan int)
go func() {
adone, bdone := false, false
for !adone || !bdone {
select {
case v, ok := <-a:
if !ok {
adone = true
continue
}
c <- v
case v, ok := <-b:
if !ok {
bdone = true
continue
}
c <- v
}
}
}()
return c
}
This looks like it might work! Letās run it.
> go run main.go
1
2
3
4
5
6
7
8
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/Users/francesc/src/github.com/campoy/campoy.cat/site/static/code/nilchans/main.go:13 +0x186
exit status 2
Ooops, we forgot something. What could that be? Well, we can see that thereās only one go routine running and itās blocked on line 13. That line is:
for v := range c {
Can you see what the problem is? Well, a range
statement iterates over all the values in a channel until the channel is closed. But who is closing the channel?
We forgot! Letās add a defer
statement in our go routine to make sure the channel is closed eventually.
func merge(a, b <-chan int) <-chan int {
c := make(chan int)
go func() {
defer close(c)
adone, bdone := false, false
for !adone || !bdone {
select {
case v, ok := <-a:
if !ok {
adone = true
continue
}
c <- v
case v, ok := <-b:
if !ok {
bdone = true
continue
}
c <- v
}
}
}()
return c
}
Note that the defer
statement is inside of the anonymous function called in a new go routine, rather than inside of merge
. Otherwise c
would be closed as soon as we exited merge
and sending a value into it would panic.
Letās run it and see what happens.
> go run main.go
1
2
3
4
5
6
7
8
This looks great ā¦ but is it?
The code we wrote so far is pretty good. It is functionally correct, but if you deployed this in production you might end up running into performance troubles.
In order to show you where the problem is, letās add a bit of logging.
func merge(a, b <-chan int) <-chan int {
c := make(chan int)
go func() {
defer close(c)
adone, bdone := false, false
for !adone || !bdone {
select {
case v, ok := <-a:
if !ok {
log.Println("a is done")
adone = true
continue
}
c <- v
case v, ok := <-b:
if !ok {
log.Println("b is done")
bdone = true
continue
}
c <- v
}
}
}()
return c
}
Letās run it and see what happens.
> go run main.go
2
3
4
5
6
7
8
a is done
2018/01/14 20:47:22 b is done
... š±
2018/01/14 20:47:23 b is done
2018/01/14 20:47:23 a is done
Uh oh! It seems once a channel is done we keep on iterating non-stop!
It does make sense after all. As we saw at the beginning reading from a closed channel never blocks. Therefore the select
statement will block as long as both channels are open until a new element is ready, but once one of them closes we will iterate and waste CPU. This is also known as a busy loop, and itās not good.
In order to avoid the busy loop describe previously we would like to disable a part of the select
statement. Concretely, weād like to remove case v, ok := <- a
when a is closed and similarly for b
. But how?
As we mentioned at the beginning, receiving from a nil channels blocks forever. So to disable a case
receiving from a channel, we can simply set that channel to nil
!
We can then stop using adone
and bdone
and instead check for a
and b
being nil
.
func merge(a, b <-chan int) <-chan int {
c := make(chan int)
go func() {
defer close(c)
for a != nil || b != nil {
select {
case v, ok := <-a:
if !ok {
fmt.Println("a is done")
a = nil
continue
}
c <- v
case v, ok := <-b:
if !ok {
fmt.Println("b is done")
b = nil
continue
}
c <- v
}
}
}()
return c
}
Ok, hopefully this will avoid unnecessary loops. Letās try it.
> go run main.go
2
1
4
3
6
5
8
7
b is done
a is done
The code for the final solution is on GitHub.
This is just one of the many concurrency patterns that can benefit from nil channels. Have you used nil channels to solve some other problems? Share your story! You can get in touch with me on twitter or simply dropping a comment here.
And you might wonder ā¦ how do we merge more than two channels? Well, wonder no more and go read the next episode that covers exactly that!
If you enjoyed this episode make sure you share it and subscribe to justforfunc! Also, consider sponsoring the series on patreon.
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!