Video summary
Как на самом деле устроены каналы в Golang? | Golang channels internals
Main summary
Key takeaways
Main ideas / lessons
- Go channels are not magic: they’re built from well-understood data structures and concurrency primitives in the Go runtime.
-
Channels exist to provide key properties that help developers write correct concurrent code without manually managing synchronization:
- Thread safety: multiple goroutines can interact with a channel safely.
- Buffering + FIFO semantics: channels can store elements and preserve first-in-first-out ordering.
- Blocking behavior: reads/writes block when the channel state doesn’t allow the operation to proceed (e.g., reading from an empty channel).
-
Internally, a channel is represented by a runtime structure containing:
- Buffer state and size (for buffered channels)
- Closed flag
- Indices for where to read/write in the circular buffer
- Synchronization (mutex)
- Wait queues for goroutines that must be parked because they can’t proceed immediately
Implementation concepts (what structures do what)
1) Channel structure (runtime fields)
The conceptual fields that make up a channel include:
buffer: the underlying buffer storage (for buffered channels).dataqsiz(buffer capacity) / “size of buffer”.closedflag (noted as stored in auint32-like representation due to atomic/processor constraints).sendxandrecvx(or equivalent): indices into the circular buffer:sendxindicates where the next send writes.recvxindicates where the next receive reads.
mutex(lock/unlock): ensures safe updates to shared state.recvqandsendq: pointers to linked lists / queues of goroutines waiting to receive or send.type/ element size info: describes what element type the channel holds.
Also noted:
- Channels are referenced as a pointer to the structure (the “channel is just a pointer” idea).
2) Buffer as a circular queue (FIFO + efficient wraparound)
For buffered channels:
- The buffer is a circular queue:
sendxwrites intobuffer[sendx], then increments.- When
sendxreaches the end, it wraps back to0. recvxreads frombuffer[recvx], then increments.- When
recvxreaches the end, it wraps back to0.
- This avoids expensive shifting; indices wrap instead of moving memory.
3) Queueing parked goroutines: the “sudog/court” concept
When a goroutine can’t proceed:
- It is parked (put to sleep) and placed into:
- the send queue (
sendq) if it’s a sender waiting to send - the receive queue (
recvq) if it’s a receiver waiting to receive
- the send queue (
- Each queued entry is described as a “court” (court structure / sudog in Go runtime terms), which includes:
- a link into a linked list (queue management)
- a field that stores the waiting goroutine state
- either:
elem: the element being sent (for sender waiters), or- a pointer/location where the receiver wants to place the received element (for receiver waiters)
Detailed instruction-style flow (send/receive with blocking and wakeup)
A) Buffered channel: send arrives when space exists
- Sender locks the channel mutex.
- It checks:
- whether the channel is closed (panic if closed)
- whether there is free buffer space
- If buffer has space:
- Copy the sender’s value into
buffer[sendx]. - Increment
sendx(wrapping via circular queue logic).
- Copy the sender’s value into
- Unlock the mutex.
- Send completes.
Key detail emphasized:
- The send copies the value into the channel buffer (sender and receiver don’t share the same memory for that value—though the speaker notes reference-type caveats).
B) Buffered channel: receive arrives when data exists
- Receiver locks the channel mutex.
- It checks:
- channel closed semantics as needed
- whether buffer contains elements (
count > 0)
- If buffer has data:
- Copy from
buffer[recvx]into receiver’s destination. - Clear/mark the buffer cell as empty (conceptually).
- Increment
recvx(wrapping via circular queue logic).
- Copy from
- Unlock the mutex.
- Receive completes.
C) Buffered channel: sender blocks when buffer is full
- Sender attempts to send but finds buffer is full.
- The send operation parks the goroutine:
- calls
gopark-like mechanism (go parkin subtitles).
- calls
- The goroutine is placed into the channel’s send queue (
sendq). - The scheduler runs other goroutines until a matching receiver arrives.
D) Buffered channel: receiver unblocks a parked sender (wake optimization)
When a receiver later reads and creates buffer space:
- Receiver takes the needed element from buffer (FIFO order).
- If a waiting sender exists in
sendq, the receiver directly helps:- removes a sender request from
sendq - places that sender’s element into the newly freed buffer cell
- advances
sendxappropriately
- removes a sender request from
- Receiver then calls the “unpark”/
goready-like mechanism:- Sender goroutine becomes runnable.
- Optimization claimed:
- Receiver performs the waking/transfer work so the sender doesn’t redo the entire send logic.
- Mutex is locked once; handoff avoids extra locking steps.
E) Buffered channel: receiver blocks when buffer is empty
- Receiver attempts to receive but finds buffer empty.
- Receiver parks:
- calls
gopark-like mechanism.
- calls
- The receiver is placed into the channel’s receive queue (
recvq). - It sleeps until a sender arrives.
F) Buffered channel: sender unblocks a parked receiver (and reduces copying)
When a sender arrives while receiver is parked:
- Instead of copying into buffer and then from buffer to receiver, the speaker says Go uses an optimization:
- a direct transfer between goroutines’ memory locations (described as stack-to-stack movement via a special mechanism, “only place where this is possible”).
- Net effect:
- reduced copying and improved performance
Unbuffered (rendezvous) channels: direct transfer
For unbuffered channels:
- There is no buffer.
- Send and receive must rendezvous directly:
- If sender arrives first: it parks in
sendqwaiting for a receiver. - If receiver arrives first: it parks in
recvqwaiting for a sender. - When the other side arrives, the value is transferred directly (no intermediate buffering).
- If sender arrives first: it parks in
- Emphasis:
- direct handoff occurs “regardless of who came first.”
Select construct (non-blocking multi-channel operation)
selectconceptually:- Randomly permutes the order of cases to check (non-deterministic ordering).
- Tries cases sequentially to find one that can proceed.
- If a case is readable/writable immediately, it executes it.
Key detail described:
- Reads in
selectare performed withblock=falsesemantics so the runtime can test without parking. - If no cases are ready:
- if there is a
default, it runsdefault(no parking) - otherwise the goroutine parks (implied by blocking vs non-blocking behavior)
- if there is a
Closing channels: semantics and runtime actions
What closing does (high level)
Closing a channel:
- sets a closed flag
- wakes/affects goroutines waiting in both queues:
- Receivers waiting to read:
- released and subsequently observe “zero values”
- in Go: receives from a closed channel proceed immediately with zero value once buffer is drained
- Senders waiting to send:
- released but should panic when they attempt to send to a closed channel
- Receivers waiting to read:
Step-by-step behavior (as described)
- Verify channel isn’t nil; panic if invalid.
- Lock the mutex.
- If already closed, panic.
- Mark channel as closed.
- Drain/wake waiting receivers from
recvq:- unlock them so they can proceed reading “zero forever” behavior once empty.
- Drain/wake waiting senders from
sendq:- unlock them; they panic because the channel is closed.
- Unlock/finish closure.
Where the speaker points in runtime code (sources within Go)
The Go runtime implementation is referenced as:
- package:
runtime - file:
chan(for channel internals) - file:
selectlogic in something like.../select ...(subtitles mention an unclear filename, but it clearly refers to runtime select implementation)
Also mentioned:
- channel read function (not named cleanly in subtitles)
gopark/ scheduler park- wake via scheduler “ready”/
goready-like behavior
Practical examples shown (via debugging)
- Demonstrates a buffered channel of size 4, observing:
count/ viewcount (how many elements are in buffer)- buffer contents
sendx,recvx- closed flag behavior after calling
close
- Shows blocking behavior:
- reading from empty causes blocking (observed via breakpoints)
- filling/overflow leads to send blocking
- Shows closure behavior:
- writing to a closed channel panics
- inspecting queued sender/receiver structures and their linkage
Summary of the four main properties claimed to be implemented
- Thread safety: mutex + runtime coordination.
- FIFO + buffering: circular buffer with
sendx/recvxindices and element count. - Data transfer between goroutines:
- buffered: copy into buffer
- unbuffered: direct handoff
- optimizations reduce extra copying when unblocking waiters
- Blocking/unblocking:
goparkto sleepsendq/recvqqueues to remember who is waiting- scheduler wakeup when the channel state changes or when a direct rendezvous is possible
Speakers / sources featured
- Speaker: narrator/author of the YouTube video (not explicitly named in the subtitles).
- Primary external source: Go language runtime implementation:
runtimepackage, channel code in achanfile (as per subtitles)- scheduler concepts:
gopark-like and wake (goready-like) mechanisms
- Mentions:
- processor/architecture constraints (atomic/word access constraints)
- a search for “some concept” likely related to memory ordering/atomic operations (exact term unclear due to subtitle errors)