Go provides channels as a way to communicate between two goroutines.
In essence a channel is a synchronised queue, where a producer writes to the channel, implicitly using the mutex that is hidden inside it to ensure that no other producer interferes with the data being written, and the consumer does not remove a partial copy, the consumer then reads from the channel, again using the mutex within, this time to ensure that the data being consumed (it’s removed from the channel) is not altered by another producer.
A user won’t see all of this complexity, to them it’s as simple as
// create a channel
c := make(chan c int)
// pass the channel to another goroutine
go consumer(c)
// write to a channel
c <- 5
func consumer(chan c int){
// read from the channel
fmt.Println(<-c)
}
I’ve not added the code for ensuring that the main goroutine doesn’t exit before the consumer, and any other child goroutines, exit - that’s left as an exercise for the reader.
When defined as above the consumer(s) will wait for a producer(s) to place data in the channel before moving on to its next job, that is, the consumer(s) will block, waiting for data from a producer(s).
The producer(s), too, will block if the channel has no consumer(s) ready to read from it.
What if we decided that the producer(s) can write to the channel, without needing a consumer ready and waiting? Instead of waiting for a consumer we want the producer(s) to be able to write whatever it needs to write, then to go about its business, leaving the consumer(s) to get to the data when ready.
Removing this synchronisation can be achieved with a buffer. A buffer is some memory associated with the channel that the producer(s) can write to that holds data until the consumer(s) are ready to use it.
Buffered channels are created in Go the same as non-buffered channels, with the exception that the number of items that the buffer can store is specified.
bufChan := make(chan int, 10)
Because of the buffer the channel’s not restricted to use between two or more goroutines, as the producer can write to the channel, then move on to its next job which may be consuming from that same channel.
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
Now, for the important part. Buffers are only useful if the consumer(s) can catch up on what’s being added by producer(s).
If the buffer is full, then the producer(s) will have to wait for room in the buffer to become available before they can add data. That means that a full buffer will only let producer(s) add to the channel/buffer at the exact same rate that the consumer(s) can empty the buffer.
Thus, buffered channels (in fact, all buffered queues) are for two types of situations, firstly to allow asynchronous processing, that is, producers can write and don’t need to wait for the consumers, but this is only useful when producers aren’t overwhelming the consumers. Secondly where there are bursts of activity from producers, with subsequent lulls where the consumers can catch up.