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

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Constant lengths and expressions

A few of the built-in functions are very special, in that they can evaluate to constant expressions. len and cap are two such functions. But they aren’t always evaluated to constant expressions, sometimes they’re more normal-ish runtime functions. Length and capacity … The expression len(s) is constant if s is a string constant. The expressions len(s) and cap(s) are constants if the type of s is an array or pointer to an array and the expression s does not contain channel receives or (non-constant) function calls; in this case s is not evaluated.


Capacity of slices

Length and capacity … The capacity of a slice is the number of elements for which there is space allocated in the underlying array. At any time the following relationship holds: 0 <= len(s) <= cap(s) Recall that a slice is backed by a fixed-length array, which may have more elements than the slice. When this is the case, the capacity of the slice is said to be the current length of the slice, plus any unused elements in the backing array that extend beyond the final element of the slice.


Length and capacity

Length and capacity The built-in functions len and cap take arguments of various types and return a result of type int. The implementation guarantees that the result always fits into an int. Recall that int is either a 32- or 64-bit integer. So this means that the theoretical maximum length or capacity of the various types that support len and cap depends on the CPU architecture. However, this should not matter in practice, since you’d quickly exceed the available memory, before you had a slice, array, map, or other item with 2^32 elements in it.

Get daily content like this in your inbox!

Subscribe