Channel Directionality

This lesson discusses different patterns for the implementation of channels for communication between goroutines.

A channel type may be annotated to specify that it may only send or only receive data:

var send_only chan<- int // data can only be sent (written) to the channel
var recv_only <-chan int // data can only be received (read) from the channel

Receive-only channels (<-chan T) cannot be closed, because closing a channel is intended as a way for a sender goroutine to signal that no more values will be sent to the channel. Therefore, it has no meaning for receive-only channels. All channels are created bidirectional, but we can assign them to directional channel variables, like in this code snippet:

var c = make(chan int) // bidirectional
go source(c)
go sink(c)
func source(ch chan<- int) {
  for { ch <- 1 } // sending data to ch channel
}
func sink(ch <-chan int) {
  for { <-ch } // receiving data from ch channel
}

Channel iterator pattern

This pattern can be applied in the common case where we have to populate a channel with the items of a container type, which contains index-addressable field items. For this, we can define a method Iter() on the content type which returns receive-only channel items (Iter() is a channel factory), as follows:

func (c *container) Iter () <-chan items {
  ch := make(chan item)

  go func () {
    for i := 0; i < c.Len(); i++ { // or use a for-range loop
      ch <- c.items[i]
    }
  } ()
  return ch
}

Inside the goroutine, a for-loop iterates over the elements in the container c (for tree or graph algorithms, this simple for-loop could be replaced with a depth-first search). The code, which calls this method can then iterate over the container, like:

for x := range container.Iter() { ... }

which can run in its own goroutine. Then, the above iterator employs a channel and two goroutines (which may run in separate threads).

If the program terminates before the goroutine is done writing values to the channel, then that goroutine will not be garbage collected; this is by design. This seems like the wrong behavior, but channels are for thread-safe communication. In that context, a hung goroutine trying to write to a channel that nobody will ever read from is probably a bug and not something you’d like to be silently garbage-collected.

Pipe and filter pattern

A more concrete example would be a goroutine processChannel, which processes what it receives from an input channel and sends this to an output channel:

sendChan := make(chan int)
receiveChan := make(chan string)
go processChannel(sendChan, receiveChan)
func processChannel(in <-chan int, out chan<- string) {
 for inValue := range in {
   result:= ... // processing inValue
   out <- result
 }
}

By using the directionality notation, we make sure that the goroutine will not perform unallowed channel operations.

Here is an excellent and more concrete example taken from the Go Tutorial, which prints the prime numbers at its output, using filters (sieves) as its algorithm. Each prime gets its filter, like in this schema:

Get hands-on with 1400+ tech skills courses.