Don't communicate by sharing memory, share memory by communicating
— how a single pipe also does your synchronization
// shared memory — humans keep the lock order mu.Lock() result = 42 mu.Unlock() // channel — hand it over, ownership goes too ch := make(chan int) go func() { ch <- 42 }() v := <-ch
How to make one, and how to put values in and take them out. The syntax takes ten minutes, but without training your eye here, blocking and close later will all blur together.
make(chan T) — only T flows. Anything else is a compile errornil. A nil channel blocks send and receive foreverch := make(chan int) // unbuffered ch := make(chan string, 5) // buffered (5 slots) ch := make(chan Result) // structs work too ch := make(chan chan int) // channels of channels too var ch chan int // nil — never proceeds
make(chan int) and make(chan int, 1) have the same type. Yet their synchronization behavior is entirely different.
ch <- vv := <-ch<-ch means "wait until one arrives"v, ok := <-ch, ok tells you if the channel is still alivefunc TestChannelSendReceive(t *testing.T) { ch := make(chan int) go func() { ch <- 42 // send }() value := <-ch // blocks until the send assert.Equal(t, 42, value) }
value := <-ch line means "stop here until 42 arrives" is the whole of this section.func TestChannelStructType(t *testing.T) { type Result struct { Value int Err error } ch := make(chan Result) go func() { ch <- Result{Value: 100, Err: nil} }() result := <-ch assert.Equal(t, 100, result.Value) assert.NoError(t, result.Err) }
A channel's essence is not "it moves values" but "it waits". Know when it stops and the unbuffered/buffered difference follows on its own.
Send and receive must be ready at the same time for the value to cross.
Whoever arrives first waits for the other. That meeting instant becomes the sync point of the two goroutines.
If the buffer has a free slot, the send finishes immediately.
Once it's full the sender waits; once it's empty the receiver waits. A queue has slipped in between.
The middle is the meeting point. Whichever side arrives first stands still until the other shows up.
The lines show where values flow. The buffer is FIFO — out in the order they went in.
func TestUnbufferedChannel(t *testing.T) { ch := make(chan int) // buffer size 0 go func() { ch <- 42 // blocks here until a receiver is ready }() time.Sleep(100 * time.Millisecond) // sender is already waiting value := <-ch // receiving frees the sender too assert.Equal(t, 42, value) }
go here?
The main goroutine ends up waiting on itself — fatal error: all goroutines are asleep - deadlock!
func TestBufferedChannel(t *testing.T) { ch := make(chan int, 3) // with no receiver, the first 3 sends finish instantly ch <- 1 ch <- 2 ch <- 3 // ch <- 4 → blocks here (buffer full) assert.Equal(t, 1, <-ch) assert.Equal(t, 2, <-ch) assert.Equal(t, 3, <-ch) }
ch <- 1 would deadlock.
cap(ch) — the buffer capacity fixed at creation. It never changeslen(ch) — how many values are queued in the buffer right nowcap 0, len 0ch := make(chan string, 5) assert.Equal(t, 5, cap(ch)) // capacity assert.Equal(t, 0, len(ch)) // values waiting ch <- "a" ch <- "b" assert.Equal(t, 5, cap(ch)) assert.Equal(t, 2, len(ch))
if len(ch) < cap(ch) { ch <- v } — another goroutine can fill it between the check and the send. That job belongs to select's default (part 3).
| Situation | Pick | Why |
|---|---|---|
| Handshake between goroutines | Unbuffered | The meeting instant is the sync point |
| Done · quit signals | Unbuffered | Signals have no reason to pile up |
| Producer / consumer speed gap | Buffered | Absorbs short bursts |
| Producer / Consumer | Buffered | Neither waits on the other every time |
| Bulk data transfer | Buffered | Fewer context switches |
Carve the direction into the type and wrong usage is caught at compile time. The runtime cost is zero and the syntax is one arrow moved.
chan<- int — send-only. Receiving is a compile error<-chan int — receive-only. Sending or closing is a compile errorchan int // bidirectional chan<- int // send-only — put in only <-chan int // receive-only — take out only func produce(ch chan<- int) { ch <- 1 } // OK func consume(ch <-chan int) { <-ch } // OK func bad(ch <-chan int) { close(ch) } // compile error
ch <- v and <-ch.// producer — send-only. cannot receive here func produce(ch chan<- int, values []int) { for _, v := range values { ch <- v } close(ch) // the sender closes } // consumer — receive-only. cannot close here func consume(ch <-chan int) []int { var out []int for v := range ch { out = append(out, v) } return out }
range. Every remaining example today is a variation on this shape.
ch := make(chan int, 5) — the conversion is automatic, so calling code doesn't change at all. That's the next slide.chan int → chan<- int
chan int → <-chan int
Assignment and argument passing both just work. No cast needed.
chan<- int ⇸ chan int
<-chan int ⇸ chan int
Once narrowed, permission can't be undone. To widen you must still hold the original bidirectional channel.
close doesn't destroy the channel; it announces "I will send no more". Break one of three rules and it blows up as a runtime panic, not a compile error.
rangech := make(chan int, 2) ch <- 1 ch <- 2 close(ch) // no more sends fmt.Println(<-ch) // 1 — leftovers intact fmt.Println(<-ch) // 2 fmt.Println(<-ch) // 0 — all drained
func TestReceiveFromClosedChannel(t *testing.T) { ch := make(chan int, 1) ch <- 42 close(ch) // values left in the buffer come back normally val, ok := <-ch assert.Equal(t, 42, val) assert.True(t, ok) // a valid value // empty and closed → zero value + false val, ok = <-ch assert.Equal(t, 0, val) // zero value of int assert.False(t, ok) // closed marker }
0 came from a close or from a sender is known only to ok. And it doesn't block — it returns at once.
ok check or range. It leads naturally into the next slide.| Rule | If broken |
|---|---|
| The sender closes | If the receiver closes, the sender sends on a closed channel → panic |
| Never close twice | panic: close of closed channel |
| Never send on a closed channel | panic: send on closed channel |
| Receiving from a closed channel is safe | Allowed — returns zero value + false at once |
func generator() <-chan int { ch := make(chan int) go func() { defer close(ch) // the sender closes for i := range 5 { ch <- i } }() return ch // receive-only — callers can't close } for v := range generator() { fmt.Println(v) // 0 1 2 3 4 }
<-chan, so callers can neither close nor send. All three rules from 22 have turned into compile errors.
defer close(ch) sits on the first line inside the goroutine — it closes even on panic. It's also the basic brick of pipelines (part 3).The receiver's idiom range, and the value-less channel chan struct{}.
Both sit on top of the close we just learned.
for v := range ch — waits for values and pulls them out one by onev, ok := <-ch, but it does the ok check for youch := make(chan int, 5) go func() { for i := 1; i <= 5; i++ { ch <- i } close(ch) // without it range never ends }() for v := range ch { // 1 2 3 4 5 out = append(out, v) }
range, you must also decide who closes that channel, and when. They're one set.
struct{} has no fields, so it's 0 bytes — the type shows you mean to carry no valuechan bool, "what does true mean, and false?" always followsdone, quit, readydone := make(chan struct{}) go func() { // do the work... close(done) // done signal }() <-done // wait until it's done
unsafe.Sizeof(struct{}{}) is 0 — the values crossing cost nothing. (The channel itself is a separate cost.)
Wakes exactly one waiting receiver.
Three waiting means sending three times. That is, the sender must know the receiver count.
Everyone waiting wakes at once.
Any number is fine, and receivers that arrive after the close pass through immediately. The signal can't be missed.
context.Done() works exactly this way — we meet it again in part 5.
| Expression | Meaning | Analogy |
|---|---|---|
int | the int type | blueprint |
42 | an int value | the thing |
struct{} | empty struct type | blueprint — with no fields |
struct{}{} | empty struct value | the thing — with no contents |
struct{} + {} — type first, empty literal second. Exactly like Person{} being Person + {}.
So make(chan struct{}) takes a type and done <- struct{}{} takes a value.
Everything so far, gathered into one piece of code. The moment there are several producers, "who closes" turns into a real design question.
func TestProducerConsumer(t *testing.T) { ch := make(chan int, 10) var results []int go func() { // Producer — makes squares for i := 1; i <= 10; i++ { ch <- i * i } close(ch) // declares: all sent }() for val := range ch { // Consumer — collects until closed results = append(results, val) } assert.Len(t, results, 10) }
ch := make(chan int, 20) var wg sync.WaitGroup for p := range 3 { // 3 producers wg.Add(1) go func() { defer wg.Done() for i := range 5 { ch <- p*100 + i } }() } go func() { // watcher — closes only when all are done wg.Wait() close(ch) }()
wg.Wait() in main and the producers stall the moment the buffer fills while main stalls on Wait — each waiting on the other, forever.
make changes the behaviorchan<- and <-chan and the compiler blocks wrong usageclose declares "no more sends" and must be done by the sender — closing twice panicsrange ends on that signalchan struct{} — to tell everyone, don't send, closeall goroutines are asleep - deadlock! → Slide 09 · 11select + default. → Slide 13panic: send on closed channel. Only the sender knows the end, so closing is the sender's job too. → Slide 22 · 23ok of v, ok := <-ch tells them apart. A closed channel returns zero value and false at once, without blocking. → Slide 21close(ch). range ends on close alone, and how many values were sent makes no difference. → Slide 25wg.Wait() closes once all are done. → Slide 31 · 32Today we handled one channel. In practice you wait on a value channel and an error channel, plus timeout and cancellation — several at once. That's the next part's topic.
default), timeouts that cut off endless waits, switching a case off with a nil channel —
everything brushed past today falls out of one construct, select. fan-in / fan-out too.
References
github.com/kenshin579/tutorials-go /golang/concurrencyadvenoh.pe.kr