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
andlen
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