Understanding golang channel range... again

In a previous article, I tried to explain my understanding of go channels interaction with ranges. Turns out my explanation was probably not clear enough because here I am, nearly a year after, struggling to achieve pretty much the same exercise.

So here we go again, on a good old trial and error fashion progress.

The goal here is to retrieve channel messages that are pushed from go routines created in a for loop.

The most naive thought would be this piece of (non working) code:

package main

import "fmt"

func main() {
	c := make(chan string)
	for _, t := range []string{"a", "b", "c"} {
		go func(s string) {
			c <- s
		}(t)
	}

	for s := range c {
		fmt.Println(s)
	}
}

And as a matter of fact, it fails miserably:

$ go run main.go
c
a
b
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        /home/imil/src/go/test/channels/main.go:13 +0x175
exit status 2

But why is that? well because the channel is never closed, and as such, the range never ends, go detects that behaviour and panics because this program can never terminate.

OK then, let’s close that channel right after the for loop!

package main

import "fmt"

func main() {
	c := make(chan string)
	for _, t := range []string{"a", "b", "c"} {
		go func(s string) {
			c <- s
		}(t)
	}
	close(c)

	for s := range c {
		fmt.Println(s)
	}
}

And try it

$ go run main.go
$

Nothing. There’s also a very good reason for that, as we close the channel on the main function, nothing blocks c, and no go routine in the for loop had the chance to finish its job before we hit the range loop, which will end immediately because the channel is now closed and there’s nothing to read from it. Note that you might see a result, but probably no more than 1 message, meaning that one go routine could be executed before reaching the range loop.

OK then. So that’s it, we need to Wait, and that’s a job sync.WaitGroup knows how to handle, this should be easy:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	c := make(chan string)
	for _, t := range []string{"a", "b", "c"} {
		wg.Add(1)
		go func(s string) {
			c <- s
			wg.Done()
		}(t)
	}
	wg.Wait()
	close(c)

	for s := range c {
		fmt.Println(s)
	}
}

But then again

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc0000141b8)
        /home/imil/pkg/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0xc0000141b0)
        /home/imil/pkg/go/src/sync/waitgroup.go:130 +0x65
main.main()
        /home/imil/src/go/test/channels/main.go:18 +0x143

goroutine 5 [chan send]:
main.main.func1(0xc0000200c0, 0xc0000141b0, 0x4b8cfb, 0x1)
        /home/imil/src/go/test/channels/main.go:14 +0x49
created by main.main
        /home/imil/src/go/test/channels/main.go:13 +0x123

goroutine 6 [chan send]:
main.main.func1(0xc0000200c0, 0xc0000141b0, 0x4b8cfc, 0x1)
        /home/imil/src/go/test/channels/main.go:14 +0x49
created by main.main
        /home/imil/src/go/test/channels/main.go:13 +0x123

goroutine 7 [chan send]:
main.main.func1(0xc0000200c0, 0xc0000141b0, 0x4b8cfd, 0x1)
        /home/imil/src/go/test/channels/main.go:14 +0x49
created by main.main
        /home/imil/src/go/test/channels/main.go:13 +0x123
exit status 2

What’s wrong this time?! Well, blocking happened again. We wg.Wait() for our go routines to end, but in turn, they are waiting for someone to read and consume c! So basically, we’ll never get pass wg.Wait(), go knows it, and panics.

Let’s print some debugging to witness this statement:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	c := make(chan string)
	for _, t := range []string{"a", "b", "c"} {
		wg.Add(1)
		go func(s string) {
			fmt.Printf("go %s, before chan\n", s)
			c <- s
			fmt.Printf("go %s, after chan\n", s)
			wg.Done()
		}(t)
	}

	wg.Wait()
	close(c)

	for s := range c {
		fmt.Println(s)
	}
}
$ go run main.go
go c, before chan
go a, before chan
go b, before chan
fatal error: all goroutines are asleep - deadlock!
[..,]
exit status 2

As you can see, we never get to see the second fmt.Printf().

What now? we need to Wait in a non-blocking manner. And this can be done using… another go routine!

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	c := make(chan string)
	for _, t := range []string{"a", "b", "c"} {
		wg.Add(1)
		go func(s string) {
			fmt.Printf("go %s, before chan\n", s)
			c <- s
			fmt.Printf("go %s, after chan\n", s)
			wg.Done()
		}(t)
	}

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

	for s := range c {
		fmt.Println(s)
	}
}

Fingers crossed

$ go run main.go
go a, before chan
go a, after chan
a
go b, before chan
go b, after chan
go c, before chan
b
c
go c, after chan

Yay! Much better. In order to witness more clearly the behavior of this method, let up add a timer to the waiting function:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	c := make(chan string)
	for _, t := range []string{"a", "b", "c"} {
		wg.Add(1)
		go func(s string) {
			fmt.Printf("go %s, before chan\n", s)
			c <- s
			fmt.Printf("go %s, after chan\n", s)
			wg.Done()
		}(t)
	}

	go func() {
		c <- "oooweeeee I'm still heeeere"
		time.Sleep(time.Second * 2) // wait 2 seconds
		fmt.Println("ran now")
		wg.Wait()
		close(c)
	}()

	for s := range c {
		fmt.Println(s)
	}
}
$ go run main.go
go a, before chan
go b, before chan
go c, before chan
oooweeeee I'm still heeeere
a
b
c
go c, after chan
go a, after chan
go b, after chan
ran now

You should see a 2 seconds wait time before ran now it displayed, and thus the channel is closed. Note that in this scenario, wg.Wait() is useless as the channel loop will consume all c’s way before 2 seconds.

So that’s it! I hope I made this clearer to my own mind… and maybe yours ;)