Go Channels

From NovaOrdis Knowledge Base
Jump to navigation Jump to search

External

Internal

TODO

Deplete Go_Channels_TODEPLETE into this.

Overview

Go channels provide a composable, concurrent-safe and type-safe way to communicate between concurrent processes. A channel serves as a conduit for a stream of information: values may be written on the channel and then read out downstream. For this reason, it's probably not a bad idea to end the channel variable names with the word "Stream". When using a channel, you pass a value into a chan variable and then somewhere else in your program read it off the channel. The program part that reads it does not require knowledge of the part that wrote it, only the channel variable.

Channels are typed: the data that goes through a channel has a specific type.

Because channels are composable with other channels, this makes writing large systems simpler. You can coordinate the input from multiple subsystems by easily composing the output together. You can combine input channels with timeouts, cancellations, or message to other subsystems. The select statement is the complement to Go's channels. It is what enables all the difficult parts of composing channels. select statement allows you to wait for events, select a message from competing channels in a uniform random way, continue on if there are no messages waiting, and more. Channels orchestrate, mutexes serialize.

Also see:

Go Concurrency Programming Models

R chan W

R <- chan <- W

The data flows between the channel and the variable in the direction the arrow points. See Sending on Channels and Receiving from Channels below.

Declaration and Instantiation

The channel variables are declared with the usual var keyword declaration syntax, where the channel type can be a Go built-in type or a user-defined type:

var c chan <channel_type>

The declaration shown above assigns a zero value bidirectional channel to the variable. However, because zero value channels cannot be used for anything useful, the actual channel instances must be created with make(). The short variable declaration syntax can also be used, but only inside functions.

var c chan int // creates a zero value channel
c = make(chan int) // assigns a valid channel instance to the channel variable
c2 := make(chan int) // short variable declaration

A channel variable is a value, not a reference variable, which means that no two different channel variables may point to the same channel instance. unidirectional channels can also be declared, for a discussion around unidirectional and bidirectional channels, see Bidirectional and Unidirectional Channels below.

Channel Zero Value

A channel variable declaration creates a zero value channel, but that instance cannot be used for sending or receiving data, and it is represented with the predeclared identifier nil. Attempts to send on such channel instances block even if there are readers, so they simply cannot be used.

make()

Channel instances are created with the built-in function make():

c := make(chan string)

Invoking make() with only the chan keyword and the payload type makes an unbuffered channel: its capacity to hold objects in transit is 0. To make a buffered channel, specify an integer capacity as the third argument:

c := make(chan string, 3)

Unbuffered and Buffered Channels

An unbuffered channel cannot hold data in transit. This is the default mode for creating the channel instances. The sending operation blocks on an unbuffered channel until some other goroutine reads the data from the channel on the receiving end. For the same reason, the receiving operation blocks until some data is sent on the sending end.

Because of the blocking behavior, an unbuffered channel provides communication between threads, but also execution synchronization: the unbuffered channel can be used as a synchronization mechanism only, and the data passing on the channel simply thrown away. The language syntax supports that by allowing receiving from a channel without storing the result in any variable - which means the result will be simply discarded:

<- c

This syntax has a synchronization "wait" semantics. Also see WaitGroup.Wait().

A buffered channel can be configured to contain a limited number of objects in transit, making them "buffered channels". The capacity is specified as an argument of the make() function when the channel is initialized:

c := make(chan string, 3)

The sending operation on a buffered channel only blocks if the buffer is full. The receiving operation blocks only if the buffer is empty. The main reason for buffering is to allow sender and receiver to operate at different speeds, at least from time to time. For unbuffered channels, the sender and the receiver will work in lockstep, which reduces the concurrency of the code. For a buffered channel, the buffer can temporarily absorb some of the differences in speed between the producer and the consumer. It cannot do that forever, for its capacity is finite.

Bidirectional and Unidirectional Channels

When a channel is declared with:

var s chan <channel_type>

it is declared to be default a bidirectional channel, which means that data can be both read and written on in. A unidirectional channel is a channel that can be either read, or written.

To declare a read-only channel, use the <- operator at the left of the chan keyword.

var roChan <- chan <channel_type>

To declare a write-only channel, use the <- operator at the right of the chan keyword.

var woChan chan <- <channel_type>

The attempt to read from a write-only channel or write on a read-only channel is signaled out by the compiler:

invalid operation: cannot send to receive-only channel roC (variable of type <-chan int)

Converting Bidirectional Channels to Unidirectional Channels

Channels that are declared bidirectional can be converted to unidirectional channels when the logic of the program requires it. The compiler will enforce the conversion by preventing unsupported operations on channels such converted. Unidirectional channels are not very often instantiated as such, but they are used as function parameter and return types.

var c chan int
c = make(chan int)

var roC <- chan int
var woC chan <- int
	
// these assignments are legal, the compiler will ensure that roC will only be used for reading
// and woC will only be used for writing
roC = c
woC = c
	
woC <- 10
i := <- roC

Closed Channels

Sending on Channels

To send data on a bidirectional or write-only channel, use the left arrow operator <- at the right of the channel variable. Note that "sending on a channel" and "writing on a channel" are used interchangeably.

c <- 10

Sending on a channel is blocking: if no goroutines are attempting to receive data from the unbuffered channel, or the buffered channel is full, the sending operation blocks until a goroutine actually receives data from the channel.

Receiving from Channels

To receive data from a bidirectional or read-only channel, use the left arrow operator <- at the left of the channel variable. Note that "receiving from a channel" and "reading from a channel" are used interchangeably.

i := <- c

Similarity to sending, receiving from a channel is blocking: if no goroutines are attempting to send data to the unbuffered channel, or the buffered channel is empty, the receiving operation blocks until a goroutine actually sends data to the channel.

The receive operation actually returns two values: the first is the value read from the channel, and the second is a boolean that says whether the value that read from the channel was generated by a write somewhere else, or it is a default value generated by a closed channel.

i, isChannelClosed := <- c

Iterative Read from a Channel

The select Statement

Channel Patterns

Pipelines

Transferring the Ownership of Data

If you have a bit of code that produces a result and wants to share that result with another bit of code, what you are really doing is transferring the ownership of that data. Data has an owner, and one way to make concurrent programs safe is to ensure only one concurrent context has ownership of data at a time. Channels are the recommended way to implement this pattern in Go, as they help us communicate this concept by encoding the intent into the channel's type. You can create buffered channels to implement a cheap in-memory queue and thus decouple the producer from consumer. This pattern also makes you code composable with other concurrent code.

Coordinate Multiple Pieces of Logic

Channels are inherently composable and preferred when communicating between different parts of your object graph. Having locks scattered throughout the object graph is far worse. Having channels everywhere is expected and encouraged. Channels can be easily composed, which is not what can be said about locks or methods that return values. It is much easier to control the emergent complexity that arises in your project if you use channels.

Timeout

Cancellation