Home Unbuffered Channels
Post
Cancel

Unbuffered Channels

Channels are Go’s mechanism for communication between goroutines(think lightweight parallel threads). One can send a message from one goroutine to the other.

Channels can be buffered or unbuffered. Channels can also be unidirectional. We will start with unbuffered channels because it is easier to reason about unbuffered channels

Unbuffered Channels

Unbuffered channels can only handle one message at a time. That is it cannot buffer any messages on the wire. If A sends to B, B has to receive the message before A can send another message, in which case A will block at send. On the other hand, B will will block waiting for a message. In a sense, you can think of it as a synchronous remote call.

Due to the its nature, unbuffered channels can be used to synchronize multiple goroutines. I personally don’t have a good use case for synchronizing parallel executions. If you do, please feel free to add a comment below and I will update the text. Also, you might be interested in this reddit discussion.

Let’s look at the simple printCountUp example from the goroutines post. Let’s split counting up and printing into two different functions. One to count up and the other to print. The only new part here will be the countChannel, which we will use to send the values from one goroutine to the other. We construct a channel for int type values by using the built-in function make(chan int). Both countUp and printOutput functions take the channel as an argument. The first channel sends the current countUp value to the channel using channelName <- value and the latter receives the value using value <- channelName in an infinite loop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func main() {
	countChannel := make(chan int)
	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		printOutput(countChannel)
	}()

	go func() {
		defer wg.Done()
		countUp(countChannel)
	}()

	wg.Wait()
}

func countUp(counts chan int) {
	for i := 0; i < 10; i++ {
		time.Sleep(500 * time.Millisecond)
		counts <- i
	}
}

func printOutput(counts chan int) {
	for {
		i := <-counts
		fmt.Printf("%d ", i)
	}
}

Executing the above code will print the numbers as expected but will panic at the end with all goroutines are asleep - deadlock!. printOutput is still waiting for a value from the channel, but nothing is being sent. It is a deadlock indeed!

Go has a mechanism to handle this issue. The sender can close the channel and thus signal the receiver that it no longer has any more information to send. We will use the built-in close function by deferring that call before we are done waiting.

1
2
3
4
5
go func() {
	defer close(countChannel)
	defer wg.Done()
	countUp(countChannel)
}()

If we execute this code with no changes to printOutput function, once we finish counting we will find ourselves in an infinite loop of printing zeros. Because receiving on a closed channel yields the zero value of its element type. Also, we are using an infinite loop. One might check for the zero value of the channel and exit the loop, but the zero value is a valid value that can come naturally through the channel. The close function has an optional secondary boolean return that reference whether the channel is open or not. By convention, we call it ok

1
2
3
4
5
6
7
8
9
func printOutput(counts chan int) {
	for {
		if i, ok := <-counts; ok {
			fmt.Printf("%d ", i)
			continue
		}
		return
	}
}

Turns out that Go has a more concise way to listen for a channel until it is closed using the range keyword.

1
2
3
4
5
func printOutput(counts chan int) {
	for i := range counts{
		fmt.Printf("%d ", i)
	}
}

Now, that we have seen the code above, it is simple to see that the same could have been without goroutines and unbuffered channel by calling one function from the other or passing one function to the other for a more generic solution. The code would have been more concise and easier to reason about. Hence, you need to justify the use of channels fo synchronization as mentioned in the beginning.

There are a couple of things to note.

  • Using <- channel without a value on the left will discard the received value.
  • Sending to a closed channel will panic

Resources

  1. Goroutines
  2. The Go Programming Language
  3. reddit: Help understanding the use cases and pitfalls of unbuffered channels.
This post is licensed under CC BY 4.0 by the author.
Contents

Goroutines

Unidirectional Channels

Comments powered by Disqus.