Channel synchronization

May 22, 2023

I’ve made the case that channels are just data types, and non-magical. So why should we care about channels, rather than just using some sort of slice, array, or custom first-in-first-out queue?

The reason is that channels provide us certain guarantees with regard to synchronization. The spec explains:

Channel types

A single channel may be used in send statements, receive operations, and calls to the built-in functions cap and len by any number of goroutines without further synchronization.

If we were to use a standard slice type, as an example, we’d need to guard all of our reads and writes to guard against data races:

sl := []string{}

go func() {
  sl = append(sl, "foo")

go func() {
  sl = append(sl, "bar") // Data race with first goroutine

go func() {
  x := sl[0]  // Data race with first and second goroutines, and might panic if s has no elements
  sl = sl[1:] //

So a channel does give us some magic, after all. I guess I lied. It handles all of this syncronization for us. It also ensures that a receive operation blocks until an element is available, and send operation blocks until space is available.

ch := make(chan string)

go func() {
  ch <- "foo"

go func() {
  ch <- "bar" // No data race

go func() {
  x := <-ch // No data race, blocks until an element is available.

And a final note on channels for now:

Channels act as first-in-first-out queues. For example, if one goroutine sends values on a channel and a second goroutine receives them, the values are received in the order sent.

Because channels operate as a first-in-first-out queue, you are guaranteed that sends and receives happen in the same order. However, there are times when the send operations are non-deterministic. In the example above, for example, there are no guarantees whether "foo" or "bar" will be sent first, since these send operations are not synchronized with each other.

Quotes from The Go Programming Language Specification Version of December 15, 2022

Share this

Related Content

Map length and capacity

Maps, like arrays and slices, have length. Map types … The number of map elements is called its length. For a map m, it can be discovered using the built-in function len and may change during execution. … This should be straight forward, but let’s consider a couple examples for good measure: x := map[string]int{"a": 1, "b": 2, "c": 3} fmt.Println(len(x)) // 3 var y map[string]float64 fmt.Println(len(y)) // 0 And somewhat deceptively, their capacity can also be set, or at least hinted at, but not read.

Type identities of other types

Type identity … Two pointer types are identical if they have identical base types. Given: type ( P = *int Q = P R *int ) P, Q, and *int are all identical to each other, but different from *int32 and R. Two function types are identical if they have the same number of parameters and result values, corresponding parameter and result types are identical, and either both functions are variadic or neither is.

Closing channels

Channel types … A channel may be closed with the built-in function close. The multi-valued assignment form of the receive operator reports whether a received value was sent before the channel was closed. Closing a channel is a simple matter of using the built-in close function: // ch must be of type `chan T` or `chan<- T`. A receive-only channel (`<-chan T`) cannot be closed. close(ch) Calling close on a channel takes immediate effect, and prevents further values from being written to the channel (any items already in the channel may still be read).