Understanding golang channel range

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")
	}()
}

run me on playground

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")
	}()
}

run me

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)
}

run me

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)
	}
}

runme

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)
	}
}

run me

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)
	}
}

run me

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)
		}
	}()
}

run me

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!