Go Channels buffering, iteration, and synchronization

Welcome to part 23 of the Go programming tutorial, where we'll be getting a little deeper into Go channels. In the previous tutorial, we covered how to use channels to send and receive values with goroutines. That said, it was just a basic example. In reality, we're likely to have questions of synchronization and iterating through known, or unknown, numbers of channel returns. Code up to this point:

package main
import "fmt"
func foo(c chan int, someValue int) {
c

In the above example, immediately, you should be wondering something along the lines of "How come we didn't need to have a wait group for these routines to finish?"

Channels by default are blocking on sending and receiving values, so they will be waited on. Let's consider though that we might not actually know how many channels we have, or maybe we just have a lot and we know we shouldn't be hard-coding the returns. Instead, we might want to iterate over the channel data, however long it might be. Let's say instead, we want to do:

package main
import "fmt"
func foo(c chan int, someValue int) {
c

Once again, one of the very few magical operators in go, range works wonders with channels too! We can iterate over a channel, where our channel is called fooVal, by doing:

We've seen this issue before. This happens when we're still waiting around for something, but there's nothing to be waited on anymore. What are we waiting for here? Well, our channel has no buffer, and the range statement doesn't have any sort of buffer or anything, it's just waiting and waiting. I am fairly impressed it even iterates at all, giving me the impression that range is actually even more magical than we were thinking. Anyway, the channel remains open. This is a problem. We can close the channel with close(), doing close(fooVal)

package main
import "fmt"
func foo(c chan int, someValue int) {
c

Oh, great, we have no more errors... but now we see nothing. Something is still wrong. You might be thinking "oh, I've seen this before, we just need to synchronize!" You are right that we need to synchronize, but we need to do it for more reasons than just because the goroutines aren't finishing before the program. We can illustrate that with adding a sleep:

This is because we're getting to the close(), not the end of the program, before the goroutines can finish and send the data. Still, however, we can use the sync package to solve our problem here. Let's import sync, create our wait group: var wg sync.WaitGroup, use wg.Add(1) before each goroutine, and do a defer wg.Done() inside the goroutine. Finally, do a wg.Wait() before we close the channel.

Finally, we want our channels to be non-blocking. We don't need them to be blocking. The blocking aspect of channels handles a form of synchronization, but we're controlling flow now, and letting the channels block each other will be problematic. Buffers on channels goes by items. In our case, we have 10 items, so we can add a buffer of 10 when we create our channel by doing: fooVal := make(chan int, 10). In this case, we need to be certain the buffer is higher than the amount of channels we're attempting to synchronize together here. We can do 10, or we could do 50, or 500. We couldn't do 5, for example.