Channels vs Mutexes — Specification¶
Table of contents¶
- Scope
- Channel semantics from the Go spec
- Mutex semantics from sync godoc
- Go memory model — happens-before for channels
- Go memory model — happens-before for mutexes
- Closure rules for channels
- Send and receive on nil channels
- Buffer rules and ordering
- Copy and pointer rules for sync types
- Cross references in the runtime source
Scope¶
This file gathers the normative statements that govern channels and sync.Mutex. It does not teach how to use them; it states what the language and standard library promise. When a behaviour described in junior.md or senior.md is "guaranteed", it is guaranteed by one of the sentences quoted here.
The two source-of-truth documents are: - The Go Programming Language Specification, sections "Channel types", "Send statements", "Receive operator", and "Close". - The Go Memory Model, sections "Channel communication" and "Locks". - Package sync godoc, types Mutex, RWMutex, plus the documented invariant in sync.Locker.
Channel semantics from the Go spec¶
"A channel provides a mechanism for concurrently executing functions to communicate by sending and receiving values of a specified element type. The value of an uninitialized channel is
nil." — Go spec, Channel types."The channel direction is part of its type; in
chan<- Tthe channel may only be sent to, in<-chan Tit may only be received from. A bidirectional channel may be implicitly converted to either directional type, but not the other way." — Go spec, Channel types."A new, initialized channel value can be made using the built-in function
make, which takes the channel type and an optional capacity as arguments." — Go spec, Making channels.
Send and receive:
"A send statement sends a value on a channel. The channel expression must be of channel type, the channel direction must permit send operations, and the type of the value to be sent must be assignable to the channel's element type." — Go spec, Send statements.
"The expression blocks until the send can proceed. A send on an unbuffered channel can proceed if a receiver is ready. A send on a buffered channel can proceed if there is room in the buffer." — Go spec, Send statements.
"A send on a closed channel proceeds by causing a run-time panic. A send on a nil channel blocks forever." — Go spec, Send statements.
"The receive operator
<-ch… blocks until a value is available. Receiving from a nil channel blocks forever. A receive operation on a closed channel can always proceed immediately, yielding the element type's zero value after any previously sent values have been received." — Go spec, Receive operator."A receive expression used in an assignment or initialization of the form
x, ok = <-chyields an additional untyped boolean result reporting whether the communication succeeded. The value ofokistrueif the value received was delivered by a successful send operation, orfalseif it is a zero value generated because the channel is closed and empty." — Go spec, Receive operator.
Close:
"The
closebuilt-in function closes a channel, which must be either bidirectional or send-only. … Sending to or closing a closed channel causes a run-time panic. Closing the nil channel also causes a run-time panic." — Go spec, Close.
Mutex semantics from sync godoc¶
"A Mutex is a mutual exclusion lock. The zero value for a Mutex is an unlocked mutex." — package
syncgodoc, typeMutex."A Mutex must not be copied after first use." — package
syncgodoc, typeMutex."If the lock is already locked for reading or writing, Lock blocks until the lock is available." — package
syncgodoc,(*Mutex).Lock."Unlock unlocks m. It is a run-time error if m is not locked on entry to Unlock. A locked Mutex is not associated with a particular goroutine. It is allowed for one goroutine to lock a Mutex and then arrange for another goroutine to unlock it." — package
syncgodoc,(*Mutex).Unlock.
For RWMutex:
"A RWMutex is a reader/writer mutual exclusion lock. The lock can be held by an arbitrary number of readers or a single writer. The zero value for a RWMutex is an unlocked mutex." — package
syncgodoc, typeRWMutex."If a goroutine holds a RWMutex for reading and another goroutine might call Lock, no goroutine should expect to be able to acquire a read lock until the initial read lock is released. In particular, this prohibits recursive read locking. This is to ensure that the lock eventually becomes available; a blocked Lock call excludes new readers from acquiring the lock." — package
syncgodoc, typeRWMutex.
sync.Locker interface:
— packagesync, typeLocker.
Both *Mutex and *RWMutex (via (*RWMutex).RLocker for read access) satisfy this interface.
Go memory model — happens-before for channels¶
The Go Memory Model (https://go.dev/ref/mem) states the following synchronization guarantees for channels. Quoting the Channel communication section:
"A send on a channel is synchronized before the completion of the corresponding receive from that channel. This rule generalizes both buffered and unbuffered channels." — Go memory model, Channel communication.
"The closing of a channel is synchronized before a receive that returns because the channel is closed." — Go memory model, Channel communication.
"The k-th receive on a channel with capacity C is synchronized before the completion of the (k+C)-th send from that channel. This rule generalizes the previous one to buffered channels: it corresponds to the fact that a buffered channel can be viewed as equivalent to an unbuffered channel together with a queue of length C; with this rule it is possible to use a buffered channel as a counting semaphore." — Go memory model, Channel communication.
"A receive from an unbuffered channel is synchronized before the completion of the corresponding send on that channel." — Go memory model, Channel communication.
Practical reading of these four rules: 1. Whatever a sender writes before ch <- v is visible to a receiver of v. 2. Whatever the closer writes before close(ch) is visible to a goroutine that observes the channel as closed. 3. With a buffered channel of capacity C, the (k+C)-th send waits for the k-th receive — that is the formal basis for "buffered channel as semaphore". 4. On an unbuffered channel only, the receive completes before the send completes; this is why a ch <- x paired with a receiving goroutine can be used as an arrival barrier.
Go memory model — happens-before for mutexes¶
"For any call to
l.Lockwherelis async.Mutexorsync.RWMutex, there is a strict total order over all precedingl.Unlockcalls, and the n-th call tol.Lockis synchronized after the n-th call tol.Unlock." — Go memory model, Locks."Any call to
(*RWMutex).RLockthat returns after a corresponding call to(*RWMutex).Unlockis synchronized after that call to(*RWMutex).Unlock. Any call to(*RWMutex).RUnlockis synchronized before the next call to(*RWMutex).Lock." — Go memory model, Locks.
Two consequences: - Whatever the n-th unlocking goroutine wrote before Unlock is visible to the (n+1)-th locker after Lock returns. - An RWMutex writer sees everything every previous reader observed under the read lock, even though readers are not mutually exclusive among themselves.
Closure rules for channels¶
Summarised from the spec and the close built-in:
| Operation | nil chan | open chan | closed chan |
|---|---|---|---|
ch <- v | blocks forever | sends (blocks until receiver / room) | panics |
<-ch | blocks forever | receives (blocks if empty) | returns zero + ok=false |
close(ch) | panics | closes | panics |
len(ch), cap(ch) | 0, 0 | current length / capacity | current length / capacity |
A receive-only channel (<-chan T) cannot be closed; close requires a bidirectional or send-only channel — enforced at compile time.
Send and receive on nil channels¶
The blocking-forever behaviour of nil channels is load-bearing for the select idiom:
var ch chan int // nil
select {
case v := <-ch: // never chosen — ch is nil
use(v)
case <-time.After(time.Second):
// always runs
}
This is how production code disables a case in a select: by setting the channel variable to nil. It is guaranteed by the spec, not an implementation detail.
Buffer rules and ordering¶
"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." — Go spec, Channel types (paraphrased; see runtime
chan.gofor the underlying ring buffer).
There is no ordering guarantee across different channels, nor across multiple senders on the same channel beyond FIFO at the channel.
Copy and pointer rules for sync types¶
The standard library's go vet rule copylocks enforces:
"Locks that are erroneously passed by value can be hard-to-find bugs. The vet check
copylocksreports a copy of any value containing async.Mutexor other lock." —golang.org/x/tools/go/analysis/passes/copylocksdocumentation.
In source: a struct embedding sync.Mutex (or any type whose Lock/Unlock methods take a pointer receiver) must be passed by pointer once it has been locked. The same applies to sync.WaitGroup, sync.Cond, sync.Once, sync.RWMutex, atomic.Value, atomic.Int64, and the noCopy marker types in the standard library.
Channels, in contrast, are reference types — copying a channel value copies a pointer to the underlying hchan. There is no noCopy on channel types.
Cross references in the runtime source¶
| Symbol | File | What it implements |
|---|---|---|
hchan struct | src/runtime/chan.go | Channel header: buffer, send/recv queues, lock |
chansend1, chansend | src/runtime/chan.go | Implements ch <- v |
chanrecv1, chanrecv2, chanrecv | src/runtime/chan.go | Implements v <- ch and v, ok <- ch |
closechan | src/runtime/chan.go | Implements close(ch) |
selectgo | src/runtime/select.go | Implements select |
Mutex, Lock, Unlock | src/sync/mutex.go | Implements sync.Mutex (state word + futex-like park) |
RWMutex, RLock, RUnlock, Lock, Unlock | src/sync/rwmutex.go | Implements sync.RWMutex (writer wait counter + reader count) |
Map | src/sync/map.go | Implements sync.Map (read map + dirty map, amortised) |
Each of these files is short (under 1000 lines) and is the most authoritative description of what these primitives actually do. When the spec is ambiguous, the runtime source is the next reference.