Understanding golang channel range
by Emile `iMil' Heitor - 2018-12-31
Here we go again with my golang self teaching, today with a topic I had hard time understanding correctly (and hope I actually did): range over channels
along with goroutines
.
First of all, let’s have a little reminder. We all know a goroutine
live its own life and must be waited for at the main level
, i.e. in this example:
package main
import "fmt"
func main() {
go func() {
fmt.Println("hello there")
}()
}
There’s very little chance you will see the fmt.Println
message, because it is very likely that the main()
function will exit before that.
We also know that the canonical way to wait for a goroutine
execution is to use a Waitgroup
:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
defer wg.Wait()
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("hello there")
}()
}
Good, now to the point, we’d like a goroutine
to send a message through a channel, easy right?
package main
import "fmt"
func main() {
c := make(chan string)
go func() {
c <- "hello there"
}()
msg := <- c
fmt.Println(msg)
}
Wait (aha…) this actually works but there’s no WaitGroup
? What sorcery is this? Well, a channel
is blocking, meaning that as long as it is waiting for data, it will block. So in a way, a channel
permits to synchronize with a goroutine
, great!
Now let’s send a couple of strings
through that channel
and receive them with a range
, which can iterate over a channel
:
package main
import "fmt"
func main() {
c := make(chan string)
go func() {
for i := 0; i < 10; i++ {
c <- "hello there"
}
}()
for msg := range c {
fmt.Println(msg)
}
}
Now that’s interesting:
hello there
hello there
hello there
hello there
hello there
hello there
hello there
hello there
hello there
hello there
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/sandbox697910326/main.go:14 +0x120
To understand what’s happening here, we must understand that a channel
range
doesn’t stop when there’s no more elements to be read unless the channel is closed. Alright then, let’s close it!
package main
import "fmt"
func main() {
c := make(chan string)
go func() {
for i := 0; i < 10; i++ {
c <- "hello there"
}
close(c)
}()
for msg := range c {
fmt.Println(msg)
}
}
Much better.
Now let’s try something a bit more tricky, let’s fire up goroutines
inside the for loop:
package main
import "fmt"
func main() {
c := make(chan string)
for i := 0; i < 10; i++ {
go func() {
c <- "hello there"
}()
close(c)
}
for msg := range c {
fmt.Println(msg)
}
}
Result:
panic: close of closed channel
goroutine 1 [running]:
main.main()
/tmp/sandbox536323156/main.go:12 +0xa0
We’re committing a double error here: closing the channel
from the receiver part, which is a forbidden, plus we try to send and close this already closed channel on second pass, which leads to a panic
.
So that’s it? No way of getting those values from this for / go
loop? Actually there is, and it involves using another goroutine
:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
defer wg.Wait()
c := make(chan string)
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c <- "hello there"
}()
}
go func() {
for msg := range c {
fmt.Println(msg)
}
}()
}
There we go, receiving data from many goroutines
! In this example, we wait for the for / go
loop to complete, the receiver goroutine
is “synchronized” with the channel and simply dies with the main program when the loop is finished sending data.
I write these articles to make things clearer for myself too, so if you spot an explanation that’s not right, do not hesitate to comment!