---
title: Concurrency
source: 'https://academia.sh/en/courses/go-concurrency'
language: en
updated: '2026-09-14T10:59:31+00:00'
license: 'CC BY-SA 4.0'
lessons: 14
outcomes:
    - 'Builds communication-based concurrency with goroutines and channels'
    - 'Propagates cancellation and timeouts with context'
    - 'Detects race conditions with tooling'
    - 'Measures how a goroutine will stop and where its failure to stop becomes visible'
---

# Concurrency

Starting a goroutine takes one line; the real question is how it will stop, and you cannot see that by reading the source. The course measures every mechanism, from channels to context cancellation, from worker pools to mutual exclusion locks, with a single question: will this goroutine stop on its own, because what it waited for arrived, because it was cancelled, or not at all?

## Goroutines and Channels

Starting a lightweight concurrent unit of execution and stopping it are two separate jobs; a channel synchronizes at the same time it carries a value; where the buffer changes blocking behavior and where it does not; branch selection when waiting on more than one channel; and the information closing carries as a termination signal.

### Goroutines

Source: https://academia.sh/en/courses/go-concurrency/goroutines

Of twelve goroutines, only 2 stopped on their own; 5 stopped because they waited for a step some other goroutine took, 2 because a context was cancelled, and 3 never stopped at all: the course's binding distribution is produced in this lesson. Starting a goroutine is one line, and that line never says when it will stop — the decision to stop usually sits not in the goroutine's own body, but in a step taken by someone else. Bounding measurement: the "stopped on its own" class is also a contract, because a goroutine whose body has finished never announces this to anyone on its own.

The Interfaces, Errors and Generics course closed by measuring where a bond forms, depending
on whether the same job was written with an interface or with a generic: eight of sixteen
bonds were carried not in the declaration or the call site, but inside the value itself, at
runtime. This lesson carries the same question into time. When the word `go` is placed in
front of a function call, a new concurrent unit of execution starts, and the calling line
moves on without waiting at all — a one-line decision that looks free. But that line never
says one thing: when and how the goroutine it started will stop.

Someone looking at the source sees the word `go`, not the condition for stopping. The
decision to stop is often not even written in the goroutine's own body; it depends on a step
taken by some other goroutine — a value sent on a channel, a lock released, a context
cancelled. This is this course's one question: how will this goroutine stop?

#### Starting and Stopping Are Two Separate Jobs

Starting a goroutine differs from calling the same function directly by a single word:
`f()` runs in the calling goroutine and blocks the caller until it returns; `go f()` runs in
a new goroutine, and the calling line moves straight on to the next line. This difference is
the entire promise the word `go` makes. When the started goroutine will finish, whether it
will finish at all, how its finishing will reach the caller — none of this is part of what
`go` commits to.

The compiler never asks about this gap at all: whether the function a goroutine calls will
ever return, and when, is not a condition for the `go` word to compile. A goroutine can run
forever, get stuck without ever running, or finish in a microsecond — the compiler accepts
all three with the same single line. This is entirely unlike the "rejected" class seen in
the Go Fundamentals course: there, the compiler refused a bond that was left unwritten;
here, it does not even ask about an unwritten bond at all.

This gap gets filled in four different ways, and the filling is usually done not by the
goroutine itself, but by **someone else**:

- the goroutine finishes its own body and stops on its own (**own**);
- it is waiting on a channel, a lock, or a wait group, and a step taken by some other
  goroutine releases it (**arrived**);
- what it is waiting for never comes, but it stops because a context was cancelled
  (**cancelled**);
- or none of this happens, and the goroutine **never stops**.

These four classes are the measurement axis for this lesson and the whole course. The name
Go gives its concurrent unit of execution is **goroutine**, and this name is used
untranslated; a goroutine is not the same thing as a thread scheduled by the operating
system — the language's own runtime runs thousands of goroutines on a small number of real
threads, with its own scheduler. This distinction itself is not this lesson's subject; the
two mechanisms are not compared here, it is only put on record once that a goroutine is a
concept in its own right.

#### A Framework That Counts Why It Stopped

The way to make the four classes measurable is to ask every goroutine to write its own
reason for stopping onto a channel, then gather the reports and print them in order —
goroutines give no order of their own, so the order has to be built into the measurement
itself. The `Ledger` type does this: `report` sends every entry onto a channel, `collect`
reads exactly the expected number of entries and sorts them by name.

```go
// stop.go — how a goroutine stops: on its own, because what it waited for arrived, because it was cancelled, or never
package main

import (
	"fmt"
	"runtime"
	"sort"
)

var own, arrived, cancelled, never int

type entry struct{ name, reason string }

// Ledger collects entries and prints them sorted; goroutines give no order.
type Ledger struct {
	channel  chan entry
	baseline int
}

func newLedger(capacity int) *Ledger {
	return &Ledger{channel: make(chan entry, capacity), baseline: runtime.NumGoroutine()}
}

func (l *Ledger) report(name, reason string) { l.channel <- entry{name, reason} }

func (l *Ledger) collect(expected int) []entry {
	r := make([]entry, 0, expected)
	for i := 0; i < expected; i++ {
		r = append(r, <-l.channel)
	}
	sort.Slice(r, func(i, j int) bool { return r[i].name < r[j].name })
	return r
}

func observe(name, reason string) {
	switch reason {
	case "own":
		own++
	case "arrived":
		arrived++
	case "cancelled":
		cancelled++
	default:
		never++
	}
	fmt.Printf("%-48s %s\n", name, reason)
}

func (l *Ledger) hasLeak() bool { return runtime.NumGoroutine() > l.baseline }

func summary() {
	fmt.Printf("stopped: %d own, %d what it waited for arrived, %d cancelled, %d never stopped\n",
		own, arrived, cancelled, never)
}
```

**GC1.** — `report` only writes onto a channel; no goroutine ever prints to the screen on
its own — every `fmt.Printf` call is inside `observe` and is run only by the main
goroutine. This separation is not arbitrary: if more than one goroutine printed to the
screen at the same time, the order the lines came out in would give no order either, and
the measurement would break itself. The `baseline` field holds what `hasLeak` compares
against — the goroutine count before the measurement began.

Gathering and sorting the order is not a matter of taste, it is a requirement that holds
across every lesson in this course: which order goroutines finish in depends on the
runtime's scheduling decision, and that decision can change from run to run. The only way to
get the same result every time is to force the order with a channel, a lock, or a wait
group, or to gather the results and sort them by our own criterion — here, the alphabetical
order of `name`. Setting a fixed wait time and assuming "it probably finishes by then" falls
into neither path; it depends on the environment's load and is never used in this lesson.

#### Twelve Goroutines, Four Ways of Stopping

The measurement uses this framework in the same package and sorts twelve goroutines into
four classes. The first two finish their own bodies and stop. The next five wait through
five separate mechanisms — a receive on an unbuffered channel, a `range` over a closed
channel, a lock being released, a wait group being zeroed out, a `select` over more than one
channel — and all five are released by a step taken by some other goroutine (or by the main
goroutine). Two stop with the cancellation of a context. The last three never stop: one
tries to send on a channel with no receiver, one ranges over a channel that never closes,
one waits on a wait group whose `Done` is never called.

```go
// measurement.go — twelve goroutines, four ways of stopping
package main

import (
	"context"
	"fmt"
	"sync"
)

func main() {
	ledger := newLedger(32)

	go func() { ledger.report("01 goroutine whose body finished", "own") }()
	go func() {
		t := 0
		for i := 0; i < 3; i++ {
			t += i
		}
		_ = t
		ledger.report("02 goroutine whose loop finished", "own")
	}()

	unbuffered := make(chan int)
	go func() {
		<-unbuffered
		ledger.report("03 unbuffered receive", "arrived")
	}()
	unbuffered <- 1

	alreadyClosed := make(chan int, 2)
	alreadyClosed <- 7
	close(alreadyClosed)
	go func() {
		for range alreadyClosed {
		}
		ledger.report("04 range over a closed channel", "arrived")
	}()

	var lock sync.Mutex
	lock.Lock()
	go func() {
		lock.Lock()
		lock.Unlock()
		ledger.report("05 waiting for the lock", "arrived")
	}()
	lock.Unlock()

	var group sync.WaitGroup
	group.Add(1)
	go func() {
		group.Wait()
		ledger.report("06 waiting for the wait group", "arrived")
	}()
	group.Done()

	ready := make(chan int, 1)
	other := make(chan int)
	ready <- 5
	go func() {
		select {
		case <-ready:
			ledger.report("07 select from the first ready branch", "arrived")
		case <-other:
			ledger.report("07 select from the second branch", "arrived")
		}
	}()

	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		<-ctx.Done()
		ledger.report("08 waiting for context cancellation", "cancelled")
	}()
	go func() {
		for {
			select {
			case <-ctx.Done():
				ledger.report("09 checking context in a loop", "cancelled")
				return
			default:
			}
		}
	}()
	cancel()

	abandoned := make(chan int)
	go func() {
		abandoned <- 1
		ledger.report("10 send with no receiver", "never")
	}()

	neverClosed := make(chan int)
	go func() {
		for range neverClosed {
		}
		ledger.report("11 range over a channel that never closes", "never")
	}()

	var neverDone sync.WaitGroup
	neverDone.Add(1)
	go func() {
		neverDone.Wait()
		ledger.report("12 waiting on a wait group that never gets Done", "never")
	}()

	fmt.Println("-- how the goroutine stopped --")
	for _, r := range ledger.collect(9) {
		observe(r.name, r.reason)
	}
	observe("10 send with no receiver", "never")
	observe("11 range over a channel that never closes", "never")
	observe("12 waiting on a wait group that never gets Done", "never")

	fmt.Println("would the send go through now          :", tryToSend(abandoned))
	fmt.Println("is a goroutine left that never stopped :", ledger.hasLeak())
	summary()
}

func tryToSend(ch chan int) bool {
	select {
	case ch <- 1:
		return true
	default:
		return false
	}
}
```

```
-- how the goroutine stopped --
01 goroutine whose body finished                 own
02 goroutine whose loop finished                 own
03 unbuffered receive                            arrived
04 range over a closed channel                   arrived
05 waiting for the lock                          arrived
06 waiting for the wait group                    arrived
07 select from the first ready branch            arrived
08 waiting for context cancellation              cancelled
09 checking context in a loop                    cancelled
10 send with no receiver                         never
11 range over a channel that never closes        never
12 waiting on a wait group that never gets Done  never
would the send go through now          : false
is a goroutine left that never stopped : true
stopped: 2 own, 5 what it waited for arrived, 2 cancelled, 3 never stopped
```

**GC2.** — The first nine lines come from the call `ledger.collect(9)` and come out sorted
by `name`; the last three are printed by hand by the measurement itself, through `observe`,
because these three never send a report at all, and `collect` could never wait for them.
The number nine, in this lesson, comes not from data but from **the writer's design** — the
writer knows from the start how many goroutines will send a report, the measurement does
not confirm this; that is the subject of the bounding measurement in the third section.

Of the twelve goroutines, **2 stopped on their own, 5 stopped because what they waited for
arrived, 2 because they were cancelled, and 3 never stopped.** This distribution is binding
for the rest of the course; later lessons will print their own slice's lines and refer back
to this number in a single sentence. The same output is produced on every run, including
with `GOMAXPROCS=1`, because no order was left to the scheduler — every step was either
forced with a channel, a lock, or a wait group, or the results were gathered and sorted.

#### Three Claims

Reading this table rests on three claims. First: **starting a goroutine does not mean it
will finish.** Only two of the twelve stopped on their own; the other ten depend on a step
taken by someone else — five on a step arriving, two on a cancellation, and three on
nothing at all, because they never stopped. Someone looking at the source sees the word
`go`, but cannot read from the source which of these ten lines will stop, or when.

Second: **not stopping raises no error.** All three leaks are silent; the program exits
normally, no warning is produced. The proof is only indirect: sending on the abandoned
channel still does not go through now either (`tryToSend` returns `false`), and a goroutine
is left over the baseline count (`hasLeak` returns `true`). The count itself is never
printed anywhere, because `runtime.NumGoroutine` can also count the runtime's own
goroutines; this is the rule — **the count of a leak is never printed in any lesson, only
its existence is written as `true`/`false`.**

Third: **what makes it stop is not the channel, it is the step someone took.** The five
"arrived" lines were each set up with a different mechanism — an unbuffered receive, a
range over a closing channel, a lock, a wait group, a `select`. The mechanism changes on
every line; what does not change is the **dependency**: in every one of them, if some other
goroutine (sometimes the main goroutine) had not taken a step, the waiting party would never
have stopped. The choice between communication and sharing is a subject for later lessons;
what matters here is that, whichever mechanism is chosen, stopping always stays dependent on
someone else's step.

These three claims can be traced line by line against the code. Lines three and four (`03`,
`04`) fall into the same class but use different mechanisms: one is a single-value receive
from an unbuffered channel, the other a `range` that runs until a closed channel drains.
Lines five and six (`05`, `06`) are also "arrived," but what is waited for is not a value
but a **state** — a lock being freed, a counter being zeroed. Line seven (`07`) uses
`select` to choose the one ready source among several; here one of the two channels was
already filled, so the choice is deterministic — had both been ready at once, the choice
would have been random, and that case is measured separately in the fourth lesson.

#### Bounding Measurement: "Stopped on Its Own" Is Also a Contract

**GC3.** — The "stopped on its own" class, even though it looks the easiest, is not free of
a contract of its own. The first and second goroutines finished their bodies and stopped on
their own — but they announced this finish to no one; if the `ledger.report` call did not
exist, the main goroutine would have had no way at all to know whether these two had
finished. The measurement itself relies on this: nine of the twelve goroutines could be
counted because they wrote their report onto a channel; the measurement waits for these
nine knowing in advance how many will come (`ledger.collect(9)`), not for the ones that
never come.

This shows that **knowing** a goroutine has finished is a job separate from its
**finishing**. When a goroutine finishes its own body, the language does nothing at all —
it publishes no event, flips no flag. Having finished is not something visible from
outside; to become visible, the goroutine itself has to write onto a channel, exactly as in
this measurement. This is why the difference between "stopped on its own" and "never
stopped" is sometimes invisible to someone looking from outside: both are silent; the only
difference is that one has genuinely finished, the other is still running.

The practical consequence of this is that the assumption "I started the goroutine, its body
is short, it must have finished by now" can never be confirmed from the source. The source
code of the first and second goroutines does not say this; the only thing that knows they
finished is the channel this lesson's measurement set up. Had the same body been written
without the `ledger.report` call, the compiler would have changed nothing, but the
measurement itself would have become impossible — there would be no way left to wait for
anything past the ninth line.

#### Summary

- Starting a goroutine (`go f()`) and its stopping are two separate jobs; the word `go`
  never states the condition for stopping.
- A goroutine stops in four ways: by finishing its own body (**own**), by waiting for
  another goroutine's step (**arrived**), through the cancellation of a context
  (**cancelled**), or by never stopping at all (**never stopped**).
- Of the twelve goroutines, **2 stopped on their own, 5 because what they waited for
  arrived, 2 through cancellation, 3 never stopped** — this distribution is the course's
  binding number.
- A goroutine is not a thread; the language's own runtime runs goroutines on a small
  number of real threads with its own scheduler.
- A leak is never printed as a count in any lesson, only its existence is written as
  `true`/`false`.
- "Stopped on its own" is also a contract: knowing a goroutine has finished is a job
  separate from its finishing, and it has to be announced separately in order to become
  visible.

#### Next Step

The first of the five "arrived" lines came from a channel: `unbuffered <- 1` was written,
and the goroutine waiting on the other side received it and stopped. But two things
happened at once in that line — a value was carried across, and two goroutines met at the
same point. The next lesson separates these two jobs: a channel carries a value and
synchronizes the sender with the receiver at the same time, and the two do not always have
to come together.

### Channels

Source: https://academia.sh/en/courses/go-concurrency/channels

A channel does two jobs at once in the same line: it carries a value from one goroutine to another, and it forces the two goroutines to meet at the same point. A send on an unbuffered channel never completes until a receiver is ready on the other side; if there is never a receiver, the attempt never goes through. Bounding measurement: a channel is not a queue, because the value carried is copied, and any change the sender makes after the send never reaches the receiver.

The previous lesson sorted twelve goroutines into four ways of stopping, and set up the
first of the five "arrived" lines with a receive on an unbuffered channel: `unbuffered <- 1`
was written, the goroutine on the other side received it and stopped. In that one line, two
separate jobs actually happened at once — an integer passed from one goroutine to another,
and two goroutines met at the same point at the same time. This lesson separates the two: a
channel does both jobs in the same line, but without measuring them separately, what the
word "synchronization" says and what the word "transfer" says run together.

A **channel** is the one way a typed value passes from one goroutine to another; it is
written `chan int`, `chan string`, and the direction of the `<-` operator separates a send
from a receive: `channel <- value` is a send, `value := <-channel` is a receive. But a
channel is not merely a carrier. It does a second job in the same line: it **synchronizes**
the sender and the receiver. This second job is what sets a channel apart from a queue or a
mailbox, and it is exactly what this lesson measures.

These two jobs coming together sits at the center of Go's approach to concurrency: instead
of reaching a shared variable through a lock, sending the value directly from one goroutine
to another. Locks and shared state are the subject of the Shared State topic; this lesson
measures only the mechanism of communication itself — what a channel does, and what it does
not.

A channel created with `make(chan int)` starts out empty and is bound to no goroutine; the
number of parties using it can be zero, one, or many. This lesson measures the simplest
case, a single sender and a single receiver. Cases where more than one goroutine shares the
same channel — many goroutines writing to the same result channel, or many goroutines
reading from the same work channel — are the subject of the Concurrency Patterns topic; the
channel's own rules are established here first, then carried over there.

#### A Send with No Receiver Never Goes Through

A send on an unbuffered channel (`make(chan T)`, no capacity) does not complete until a
goroutine is ready to receive that value on the other side. Instead of letting time pass and
assuming "probably no one is there," it is possible to **try**, using `select` together with
a `default` branch: if the send can go through right then, `select` chooses that branch; if
it cannot, it falls to `default` without waiting at all.

```go
// rendezvous.go — the two jobs of an unbuffered channel: carrying a value and blocking a receiverless send
package main

import "fmt"

func tryToSend(ch chan string, v string) bool {
	select {
	case ch <- v:
		return true
	default:
		return false
	}
}

func main() {
	channel := make(chan string)

	fmt.Println("did the send attempt go through with no receiver at all:", tryToSend(channel, "first attempt"))

	delivered := make(chan string)
	go func() {
		v := <-channel
		delivered <- "receiver got: " + v
	}()

	channel <- "hello"
	fmt.Println("send completed, receiver took delivery of the value")
	fmt.Println(<-delivered)
}
```

```
did the send attempt go through with no receiver at all: false
send completed, receiver took delivery of the value
receiver got: hello
```

**GC4.** — In the first line, no goroutine is yet waiting on `channel`; `tryToSend` returns
`false` on **every** run in this case, because there is no one waiting — this is not a
timing issue, it is the direct consequence of what an unbuffered channel is defined to be.
In the second part, a goroutine is started, and `channel <- "hello"` is this time written
directly, not inside a `select`: this line blocks until a receiver arrives and completes
once the receiver arrives. The difference matters: `select`+`default` polls a **momentary
state**, while a plain `channel <- value` waits however long it takes and stops only once it
is **certain to complete**.

It is worth noting that the rest of the program runs in a single goroutine (the main
goroutine): the receiver goroutine prints nothing to the screen at all, it only sends the
value it received back on the `delivered` channel. Every `fmt.Println` call is in the main
goroutine, and there is an order between them forced through a channel — this is why the
output is byte-for-byte the same on every run.

This measurement style is the rule for this lesson and the rest of the course: a goroutine
never prints to the screen on its own, it only writes onto a channel; printing to the screen
is always done in the main goroutine, through the value read from the channel. If two
goroutines printed to the screen at the same time, the order the lines came out in would be
just as undetermined as the goroutines themselves — a channel here is used not only to carry
a value, but also to gather the output's order into a single goroutine.

#### The Value Carried Is Copied

When a channel carries a value, it carries a **copy** of that value, not a reference to it.
This is the channels' counterpart of the copy/share distinction seen in array and slice
assignments in the Go Fundamentals course: when a struct value is sent on a channel, the
sender changing that variable afterward does not affect the copy the receiver is holding.

```go
// copied.go — a value carried through a channel is copied, the sender's later change never reaches the receiver
package main

import "fmt"

type record struct {
	Name  string
	Value int
}

func main() {
	channel := make(chan record)
	result := make(chan record)

	go func() {
		received := <-channel
		result <- received
	}()

	r := record{Name: "counter", Value: 1}
	channel <- r
	r.Value = 999
	fmt.Println("sender's own value now  :", r)
	fmt.Println("value the receiver took :", <-result)
}
```

```
sender's own value now  : {counter 999}
value the receiver took : {counter 1}
```

**GC5.** — When the line `channel <- r` completed, `r`'s value **at that moment** was
copied and handed to the receiving goroutine. The instant afterward, when `r.Value = 999`
is written, the only thing that changes is the sender's own local variable; the copy the
receiver holds knows nothing of this, and its `Value` field stays `1`. This behavior comes
from `record` being a value type, not a pointer — had a pointer been passed over the
channel (`chan *record`), what gets copied would be the pointer itself, not the data it
points to, and in that case the sender's change would also be visible to the receiver. This
lesson measures only value transfer; what does and does not get shared through copying
pointers was a separate axis in the Arrays and Slices lesson and is not repeated here.

This copying rule carries no exception: whatever the channel's own type is — an integer, a
string, a struct, even another channel — what is sent is always a copy of that value's bit
pattern at that moment. A channel never adds a pointer of its own to the value sent; if
sharing is wanted, exactly as with sending `*record` instead of `record`, the party that
wants that sharing has to choose its own type for it.

#### The Copy Is Shallow: The Slice Inside It Is Shared

What gets copied is always only the **struct itself**, not every field inside it. If a
struct carries a slice field, when the channel copies that struct it copies the slice's
**header** — length, capacity, and a pointer to the backing array — but it does not copy
the backing array itself. This is the channel-transfer counterpart of the shallow-copy
behavior seen with arrays and slices in the Go Fundamentals course.

```go
// shallowcopy.go — the struct sent over a channel is copied, but its slice field shares the same backing array
package main

import "fmt"

type basket struct {
	Label string
	Items []int
}

func main() {
	channel := make(chan basket)
	result := make(chan basket)

	go func() {
		received := <-channel
		received.Items[0] = 999
		result <- received
	}()

	s := basket{Label: "source", Items: []int{1, 2, 3}}
	channel <- s
	receivedCopy := <-result
	fmt.Println("receiver's copy      :", receivedCopy)
	fmt.Println("sender's own variable:", s)
}
```

```
receiver's copy      : {source [999 2 3]}
sender's own variable: {source [999 2 3]}
```

**GC6.** — `received` is its own copy of the `basket` value coming from the channel, and
its `Label` field is independent of the sender — changing one does not affect the other,
just as with `record.Value` in the previous measurement. But since `Items` is a slice, what
gets copied is only the three-word slice header; the backing array the header points to is
the same one the sender has. When `received.Items[0] = 999` is written, the memory location
that changes is that one backing array both variables share — this is why `s.Items[0]` also
shows `999`, even though no goroutine ever touched the `s` variable itself directly. The
reason this line can be safely read in the main goroutine is that the `<-result` receive, by
the channel operation's own synchronization promise, also makes the write to the backing
array visible; the channel carries not only the `result` value, but everything in memory
leading up to it.

This shows the limit of the sentence "a value carried through a channel is copied": the
copying **ends with the struct itself**, it does not extend to the data pointed to by a
slice, map, or pointer field inside it. Code that wants to send a struct as a copy fully
independent of the channel has to separately copy the slice field with `copy` too — the
channel does not do this on its own.

#### A Channel's Direction Can Be Part of Its Type

A channel variable can be used in both directions, but a function's signature can restrict a
channel's direction: `chan<- int` is a channel type that allows only sending, `<-chan int`
only receiving. This restriction is part of the type itself — a bidirectional channel,
created with the same `make(chan int)`, can only be used in the allowed direction inside a
function once it is passed as a directional parameter.

```go
// directional.go — a channel's direction can be part of its type: send-only, receive-only
package main

import "fmt"

func produce(target chan<- int, count int) {
	for i := 1; i <= count; i++ {
		target <- i
	}
	close(target)
}

func sum(source <-chan int) int {
	total := 0
	for v := range source {
		total += v
	}
	return total
}

func main() {
	channel := make(chan int)
	go produce(channel, 5)
	fmt.Println("sum of the five produced values:", sum(channel))
}
```

```
sum of the five produced values: 15
```

**GC7.** — The channel passed to the `produce` function, even though it is a bidirectional
variable, appears inside `produce` only as `target chan<- int`; trying to write
`<-target` in `produce`'s body does not compile. Likewise, a send attempt like
`source <- 1` inside `sum` does not compile either. This is a writing decision that makes
which side does which job with the channel readable straight from the signature: someone
looking at `produce`'s signature knows, without reading the body, that it will only write to
the channel, never read from it. `produce` itself also closes `target` — who is responsible
for closing is not this lesson's subject, it is what the topic's closing lesson measures.

The direction restriction changes nothing at runtime; `chan<- int` and `<-chan int` share
the same underlying channel value, they are not a different channel type. What is gained is
readability at compile time alone: a function's signature commits to what that function will
do with the channel, and a call that breaks this commitment does not compile. The `channel`
variable inside `main` is passed to both `produce` and `sum` in the same bidirectional
form; the restriction only applies **inside** those functions themselves.

#### Two Measurements Together: Carrying and Meeting

**GC8.** — When these two measurements are read together, a channel's real promise comes
into view: a channel operation both freezes and carries a value's state **at that moment**,
and meets the sender and the receiver at the same point. Likening a channel to a queue gives
a useful intuition at first glance — values go in one end, come out the other — but a queue
never meets the sender with the receiver, it only separates them through a buffer. An
unbuffered channel has no such buffer; the meeting takes the buffer's place. The next two
lessons measure how this meeting gets **deferred** once a buffer is added.

These two jobs do not always carry the same weight. In some uses, only the value itself
matters — the result of a computation, a record's content. In others, what the value is does
not matter at all, only the information that **something happened**; in that case a type
carrying no data at all, like `chan struct{}`, is preferred, and the channel serves only the
synchronization job. This lesson's `delivered` channel in `rendezvous.go` is of the first
kind; the closing signals we will see a few times across the rest of the measurement are of
the second.

A channel also has an unwritten state: a channel variable's zero value is `nil`, and a send
or receive on a `nil` channel never completes, it blocks forever. This behavior is defined,
and it passed by in a single sentence in the context of zero values in the Go Fundamentals
course; this course does not build it up again. But a `nil` channel's property of "never
being ready" produces a useful result inside `select`: a branch containing a `nil` channel
is never chosen unless that channel is replaced with another value — so to temporarily
"close off" a branch, it is enough to turn that branch's channel into `nil`.

```go
// nilchannel.go — every operation on a nil channel blocks forever, always passed over inside select
package main

import "fmt"

func main() {
	var nilChannel chan int
	ready := make(chan int, 1)
	ready <- 7

	select {
	case v := <-nilChannel:
		fmt.Println("received from the nil channel:", v)
	case v := <-ready:
		fmt.Println("received from the ready channel:", v)
	default:
		fmt.Println("neither is ready")
	}
}
```

```
received from the ready channel: 7
```

`nilChannel` was never initialized with `make`, so it stayed at its zero value, `nil`.
`select` polled all three branches: the `nilChannel` branch is ruled out because it can
never be ready; the `ready` branch is full and gets chosen. Had `<-nilChannel` been written
directly and called outside a `select`, the program would have stayed there forever; this
lesson never runs that case, because its result (blocking forever) produces no observable
output. The `select` statement itself is measured in detail in the next lesson; what matters
here is that a `nil` channel can be used as a way to make a branch ineffective.

#### Summary

- A channel does two jobs in the same line: it carries a value and synchronizes the sender
  with the receiver; the two always come together.
- On an unbuffered channel with no receiver, a send attempt (`select`+`default`) never
  goes through; this is not a timing coincidence, it is the definition itself.
- A send written directly (`channel <- value`) blocks until a receiver arrives and
  completes for certain once the receiver arrives.
- A value carried through a channel is copied; the sender's change after the send never
  reaches the receiver.
- Every operation on a `nil` channel blocks forever; this behavior is the channels'
  counterpart of a definedness established in the Go Fundamentals course.

#### Next Step

The unbuffered channel measured in this lesson kept the sender waiting until the receiver
was ready — the meeting was set up at exactly that moment, every time. Adding capacity to a
channel does not remove this requirement, it only defers it. The next lesson measures where
the buffer changes blocking behavior and where it does not: a send goes through without
waiting at all until capacity fills, and once it fills, it waits again.

The question to ask is this: does the requirement to meet disappear entirely, or does only
**when** it happens change? The answer lies in separating a send to a buffered channel
returning **immediately** from the fact that the value is **still waiting to be read**.

### Buffered and Unbuffered Channels

Source: https://academia.sh/en/courses/go-concurrency/buffered-and-unbuffered-channels

A channel's capacity changes where blocking starts: a send passes without waiting at all until capacity fills, and once it fills, it blocks exactly like an unbuffered channel. How many of five attempted sends pass without blocking is measured at three capacities. Bounding measurement: the buffer does not remove synchronization, it only defers it — once a send to a buffered channel completes, the sender still does not know whether the receiver has taken that value.

The previous lesson measured that an unbuffered channel does two jobs at once: carrying a
value and making the sender and the receiver meet at the same point. Adding capacity to a
channel does not remove these two jobs — it only loosens the bond between them. This lesson
measures **where** the buffer changes blocking behavior, and bounds what it does not change
in a separate section.

Giving a channel capacity turns it into a structure that holds a small array inside itself.
The existence of this array gives the sending side a kind of flexibility: even if the
receiver is not yet ready, a send can proceed without waiting as long as the buffer still
carries an empty slot. But this flexibility has a limit, and that limit is exactly the
capacity itself. This lesson measures that limit and where it gets crossed.

Capacity is fixed once, when the channel is created, and cannot be grown or shrunk
afterward — a channel opened with `make(chan int, 3)` can hold at most three values for its
entire lifetime. This fixedness shows that capacity is not a runtime setting but a design
decision: the person writing the code decides from the start how far the sender is allowed
to "get ahead of" the receiver. There is no single rule for the right size of this decision
— too small a capacity makes the sender wait more often than necessary, too large a capacity
hides how far behind the receiver has fallen; this lesson measures not how the size should
be chosen, but what a chosen size does.

#### Capacity Is Where Blocking Starts

The second argument in the call `make(chan T, n)` is the channel's capacity; if it is left
out (`make(chan T)`), the capacity is zero, and the previous lesson's unbuffered channel is
what results. An unbuffered channel is not a separate type — it is a channel whose capacity
is zero, and a zero capacity produces the same result as "the buffer is always full": every
send waits directly for a receiver, exactly as on a channel whose capacity is already full.

This single difference is what lets both channel uses be written with the same syntax: when
a function takes a `chan int` parameter, there is no need to tell from inside the code
whether the caller passed a buffered or an unbuffered channel — both use the same send and
receive syntax, the only difference is when they wait.

On a channel with capacity `n`, the first `n` sends complete immediately, even with no
receiver at all; the channel holds these values inside itself. The `n+1`th send, though,
means the buffer is now full, and it waits for a receiver to arrive, exactly as on an
unbuffered channel. This limit is not carried in the channel's own type but in the second
argument given to the `make` call; the same `chan int` type can be created with capacity `0`
or with capacity `1000`, and both uses look identical in the source code — the difference is
only in the `make` line.

This can be measured at three separate capacities, with five attempted sends each time,
using `select`+`default` — without starting a single goroutine, because having no receiver
at all is part of what is being measured.

```go
// capacity.go — a send goes through unblocked until the buffer fills, once full an attempt does not pass
package main

import "fmt"

func howManyGoThrough(capacity, attempts int) int {
	channel := make(chan int, capacity)
	passed := 0
	for i := 0; i < attempts; i++ {
		select {
		case channel <- i:
			passed++
		default:
		}
	}
	return passed
}

func main() {
	for _, capacity := range []int{0, 2, 4} {
		fmt.Printf("capacity=%d attempts=5 passed=%d\n", capacity, howManyGoThrough(capacity, 5))
	}
}
```

```
capacity=0 attempts=5 passed=0
capacity=2 attempts=5 passed=2
capacity=4 attempts=5 passed=4
```

**GC9.** — No receiver was ever started; the result is therefore strictly deterministic and
depends on no random timing — there is no goroutine here at all, so what is measured is not
timing but the channel's own capacity rule. With capacity `0`, no send passes — the previous
lesson's "an attempt never passes when there is no receiver at all" showing up again here.
With capacity `2`, exactly two pass, and with capacity `4`, exactly four: the `passed` count
is, in every case, **exactly equal to the capacity**, never reaching the fifth attempt at
all. Fifty attempts instead of five would not change the result — every attempt after the
buffer fills falls into `default` the same way, until a receiver arrives.

This shows exactly what the buffer does: it does not remove blocking, it defers the point
where blocking starts **by the size of the capacity**. At zero capacity there is no deferral
at all; as capacity grows, the sending side also gains a growing number of sends it can make
with no receiver at all.

This deferral is the size of the window in which the sender can operate **independently** of
the receiver. At capacity `0` there is no window at all, the sender has to wait for the
receiver on every step. As capacity grows, the sender can take that many steps in a row
without looking at the receiver's speed — but once the window closes (from the fifth attempt
onward in the `howManyGoThrough` measurement), behavior goes back to being identical to an
unbuffered channel's.

#### len and cap Show the Buffer's Instantaneous State

A channel's current fullness can also be read from inside the program: the built-in `len`
function returns the number of values waiting in the buffer, not yet received; the `cap`
function returns the channel's fixed capacity. The two carry independent information — `cap`
never changes from the moment the channel is created, `len` changes on every send and
receive.

```go
// lencap.go — len gives the count of values currently waiting in the channel's buffer, cap gives the capacity
package main

import "fmt"

func main() {
	channel := make(chan int, 3)
	fmt.Println("empty       : len=", len(channel), "cap=", cap(channel))

	channel <- 1
	channel <- 2
	fmt.Println("two filled  : len=", len(channel), "cap=", cap(channel))

	<-channel
	fmt.Println("one received: len=", len(channel), "cap=", cap(channel))
}
```

```
empty       : len= 0 cap= 3
two filled  : len= 2 cap= 3
one received: len= 1 cap= 3
```

**GC10.** — While the channel is empty, `len` is zero; after two sends, it is two; after one
receive, it is one. `cap` keeps holding three the whole time, unchanged, because the capacity
is fixed at the moment of `make` and cannot be changed afterward. When `len(channel) ==
cap(channel)`, the channel is full, and the next send (exactly the point measured throughout
this lesson) cannot pass without waiting; when `len(channel) == 0`, the channel is empty, and
a receive waits in the same way. These two functions are the inside-looking view of the
previous section's `howManyGoThrough` measurement: there, the number of attempts was counted
from outside; here, the channel's own state is read directly.

These functions have a limit: `len` only gives the value at the **instant** it is read, and
if more than one goroutine accesses the same channel at the same time, this value can have
already changed by the time it is read. On a channel used by a single goroutine, `len` is a
reliable observation; on a shared channel, it is only a snapshot, not a decision input.
Deciding to send, for instance, because `len(channel) < cap(channel)` holds true is not safe:
between this read and the send itself, some other goroutine can write to the same channel and
fill the buffer, and the send still waits — the "there is room" information `len` had just
given loses its validity somewhere between those two lines. `len` and `cap` are therefore an
observation tool, not a decision tool.

#### Once a Slot Opens, an Attempt Passes Again

The buffer's fullness is not fixed — the slot that opens when a receiver takes a value out
of the buffer lets an attempt that was blocked before pass now. This, too, can be observed
inside a single goroutine, in sequence, requiring no concurrency at all.

```go
// slotopens.go — once a receiver takes a value from a buffered channel, the slot that opens lets an attempt pass again
package main

import "fmt"

func tryToSend(channel chan int, v int) bool {
	select {
	case channel <- v:
		return true
	default:
		return false
	}
}

func main() {
	channel := make(chan int, 2)
	channel <- 1
	channel <- 2

	thirdAttemptWhileFull := tryToSend(channel, 3)
	fmt.Println("did the third attempt pass while the buffer was full:", thirdAttemptWhileFull)

	receivedValue := <-channel
	fmt.Println("value received                                      :", receivedValue)

	thirdAttemptAfter := tryToSend(channel, 3)
	fmt.Println("did the same attempt pass once a slot opened        :", thirdAttemptAfter)
}
```

```
did the third attempt pass while the buffer was full: false
value received                                      : 1
did the same attempt pass once a slot opened        : true
```

**GC11.** — The channel with capacity `2` is filled with two values; the third attempt
falls to `default` because the buffer is full. Once a value is taken out with `<-channel`
(`1`, because a channel preserves entry order like a queue — this FIFO ordering is part of a
buffered channel's own definition, not a separate measurement), a slot opens in the buffer.
The same third attempt, with the source code entirely unchanged, now returns `true`. The
capacity stays fixed; what changes is the capacity's **fill level**.

The two measurements so far **tried** with `select`+`default`; when the attempt fails, no
send happens at all, and the program moves straight to the next line. But the real
consequence of the buffer filling up is that a send written directly, without `select`,
**genuinely waits** — exactly the behavior seen on the unbuffered channel in the previous
lesson. This cannot be shown inside a single goroutine, because the blocked party cannot
observe its own blocking; but it is possible to confirm, with a separate signal, that a
blocked send **completes for certain** once a slot opens.

```go
// blocking.go — once capacity fills a blocking send genuinely waits, and completes once the receiver takes a value
package main

import "fmt"

func main() {
	channel := make(chan int, 1)
	channel <- 1

	secondDone := make(chan struct{})
	go func() {
		channel <- 2
		close(secondDone)
	}()

	firstReceived := <-channel
	<-secondDone
	secondReceived := <-channel
	fmt.Println("first received :", firstReceived)
	fmt.Println("second received:", secondReceived)
}
```

```
first received : 1
second received: 2
```

**GC12.** — The channel with capacity `1` is filled with a single value (`1`). The started
goroutine's `channel <- 2` line cannot complete immediately, because the buffer is full, and
it waits there — the main goroutine does not observe this directly, since trying to (asking
"is it still blocked" with `select`+`default`, for instance) would itself be a race. Instead,
the main goroutine first takes the single value out of the buffer (`firstReceived`), opening
a slot for the second goroutine's waiting send, then waits on `<-secondDone` to be sure that
send has **certainly** completed. Because `close(secondDone)` runs **after** `channel <- 2`,
seeing this close guarantees the send has also gone through. The final receive
(`secondReceived`) is therefore `2` on every run.

#### Bounding Measurement: The Buffer Does Not Remove Synchronization, It Defers It

The previous two measurements showed **when** the buffer blocks. This section measures what
the buffer **does not tell**: when a send to a buffered channel completes, the sending side
does not know that the value was actually read — it only knows that it found room in the
buffer. On an unbuffered channel, these two pieces of information would arrive on the same
line, because a send only completed once a receiver was ready. On a buffered channel, these
two moments come apart.

```go
// deferred.go — a send to a buffered channel completes but does not tell the sender that the receiver took the value
package main

import "fmt"

func main() {
	channel := make(chan int, 1)
	ack := make(chan struct{})

	channel <- 42
	fmt.Println("send completed (buffered)")

	go func() {
		<-channel
		close(ack)
	}()

	<-ack
	fmt.Println("now we know the receiver took the value (separate signal)")
}
```

```
send completed (buffered)
now we know the receiver took the value (separate signal)
```

**GC13.** — The `channel <- 42` line completes without waiting at all, since the capacity is
not full, and the "send completed" line is printed right after — at this point, no receiving
goroutine has even started yet. The **only way** to learn that the receiver has actually read
the value is a separate `ack` channel; without it in the code, the sending side would have no
way left to know when the receive happens, or even whether it happens at all. This shows, at
the same time, both the guarantee a buffered channel gives the sending side and its limit:
the guarantee is only "the buffer held this," and it carries no promise about the receive at
all.

This difference shows that the choice of which channel to use is an engineering decision. If
the sender needs to know that the receive has happened — to confirm that a job has started,
for instance — an unbuffered channel or a separate acknowledgment signal is needed. If it is
enough for the sender to just leave the value and move on, and the receiver does not need to
be ready at that moment, the buffer lets the sender proceed without waiting. Both
"synchronize" — but at different moments, and with different information.

This distinction is an example of the difference between "knowing it is ready" and "being
certain it will be ready." Without the `ack` channel, the sending side in `deferred.go` would
continue after leaving the value in the buffer without learning anything — not whether the
receiver runs, not when, not even whether it ever runs at all. As capacity grows, this
uncertainty does not go away, only the number of sends left carrying it grows: a hundred
sends in a row to a channel with capacity `100` each say "completed" separately, but give not
a single piece of information about how many were actually read.

This silence on a buffered channel's part is not a defect; what the channel commits to is
only "hold this value," never "tell you that the receiver took it." Code that wants that
second piece of information has to set it up separately, exactly as in this measurement.

This lesson's four measurements come together in a single line. `capacity.go` showed that
capacity is a limit, and that up to it a send passes without waiting at all. `lencap.go`
showed that this limit and the buffer's current fullness can be read from inside the
program. `slotopens.go` and `blocking.go` gave two views of crossing that limit: one an
attempt failing and moving on, the other a real send genuinely waiting until a slot opened.
`deferred.go`, in turn, showed that none of these four measurements tell the moment a send
is **received** — capacity, fullness, blocking are all measurable on the sender's own side;
when the receive actually happens stays outside all of them.

#### Summary

- A channel's capacity determines where blocking starts: the first `capacity` sends pass
  without waiting at all, the ones after that wait for a receiver to arrive.
- A channel with zero capacity is not a separate type; an unbuffered channel is itself a
  channel with zero capacity.
- The slot that opens when a receiver takes a value out of the buffer lets an attempt that
  was blocked before pass now.
- A completed send to a buffered channel does not mean the receiver has read that value;
  this information can only be obtained with a separate signal.
- The choice between an unbuffered and a buffered channel is a design decision that depends
  on whether the sender needs to know the receive has happened.
- `len` gives the number of values currently waiting in a channel, `cap` gives the fixed
  capacity; together, they let the channel's instantaneous fullness be read without making
  an attempt.

#### Next Step

Both channel types measured in this lesson were each waiting on only a single channel; the
difference between them was in when that channel made you wait. `select`+`default` was used
in this lesson only as an attempt tool on a **single** channel — to poll whether the capacity
was full. But a goroutine often waits on more than one source at the same time: one of
several result channels, a cancellation signal, a timeout. The next lesson measures this
broader form of `select` — when more than one channel is waited on together inside the same
`select`, whichever branch is ready gets chosen, and what happens when none of them are
ready.

### select Statement

Source: https://academia.sh/en/courses/go-concurrency/select-statement

The select statement is the language's answer to waiting on more than one channel at the same time: when only one branch is ready, that branch is chosen deterministically; when none are ready and there is a default branch, that branch is chosen. Source order carries no priority. Bounding measurement: when two branches are ready at the same time, selection is random, and this case is not run, only modeled — randomness is the language's own decision.

The previous lesson measured where a channel's capacity starts blocking, and used `select`
with `default` only to poll a **single** channel. But a goroutine often waits for news from
more than one source at the same time: one of several result channels might be ready, a
cancellation signal might arrive, a timeout might run out. The `select` statement exists for
exactly this — the one structure that waits on more than one channel at the same time and
handles **whichever becomes ready first**.

This need shows up everywhere that waiting on a single channel is not enough. When a
goroutine is only waiting on one channel, writing `<-channel` is enough; but a goroutine that
wants to handle **whichever** of two separate sources arrives first cannot write this with a
single receive expression. `select` fills exactly this gap.

A `select` block contains more than one `case`, each starting with a channel operation. The
statement picks one of the ready operations and runs that branch; if none is ready and there
is a `default` branch, that runs immediately, otherwise `select` waits until at least one
branch is ready. This lesson measures how this choice gets made, where it is deterministic,
and where it is random.

Every branch of `select` can be a send or a receive; within the same `select`, some branches
can be sends and others receives, what matters is only which one can complete **at that
moment** without blocking. This is the first difference that sets `select` apart from a
`switch`: a `switch` compares a value against its branches in sequence, while `select` tries
every branch's own channel operation **at the same time** and decides based on which one is
ready. Writing a `case` that is not a channel operation does not compile; `select`'s branches
always start with a send or a receive.

The language sets no upper limit on how many branches a `select` can have; a `select` block
can wait on two channels or on ten. This lesson only uses two-channel examples, because the
rules being measured — deterministic selection, order independence, randomness — do not
depend on the number of branches; two branches are enough to show these rules and are the
easiest case to read. As the number of branches grows, the rules being measured do not
change, only the number of branches that can be ready at the same time grows; the third
section's rule about randomness also stays valid in exactly the same way when more than two
branches are ready.

#### Selection Is Deterministic When Only One Branch Is Ready

If only one of the branches inside a `select` is ready, `select` runs that branch without
considering any other option. If none are ready and there is a `default` branch, `select`
falls to it without waiting at all — this is exactly the behavior used to poll capacity in
the previous lesson.

```go
// singleanddefault.go — selection is deterministic when only one branch is ready, default is chosen when none are
package main

import "fmt"

func choose(a, b chan string) string {
	select {
	case v := <-a:
		return "a: " + v
	case v := <-b:
		return "b: " + v
	default:
		return "default: neither is ready"
	}
}

func main() {
	a := make(chan string, 1)
	b := make(chan string, 1)

	fmt.Println("when neither is ready :", choose(a, b))

	a <- "first value"
	fmt.Println("when only a is ready  :", choose(a, b))

	b <- "second value"
	fmt.Println("when only b is ready  :", choose(a, b))
}
```

```
when neither is ready : default: neither is ready
when only a is ready  : a: first value
when only b is ready  : b: second value
```

**GC14.** — All three calls call the same `choose` function, with the same source code; the
only thing that changes is the fullness of the channels. In the first call, both are empty,
and `default` is chosen. Once a value is placed on `a`, the second call chooses the `a`
branch and, in doing so, **consumes** the value on `a` — `a` is now empty again. In the
third call, with only `b` full, the `b` branch is chosen. In no call were more than one
branch ready at the same time; this is why all three results are strictly repeatable.

It matters that this measurement can be done inside a single goroutine, requiring no
concurrency at all: values are placed on the `a` and `b` channels directly from the main
goroutine, no goroutine is started. This shows that `select` itself is not a construct
specific to concurrency — `select` is only a language feature that chooses among more than
one channel operation; whether or not there is a goroutine on the other side of the channels
does not change `select`'s own behavior. In later lessons `select` will always be used
between goroutines, but this lesson shows its core behavior in its plainest form, inside a
single goroutine.

This distinction looks small but matters: `select`'s deterministic behavior comes not from
timing, only from **which channels are full at that moment**. Had the same code filled the
channels in a different order, or run on a different machine, the result would still be the
same — because what decides it is not timing, but the fullness state itself.

#### Source Order Is Not a Priority

`select`'s branches carry no top-to-bottom priority order — this is a fundamental
difference from an ordinary `if`/`else if` chain or a `switch`. Whichever branch is ready
gets chosen, no matter what order it was written in; source order exists only for
readability, it does not affect runtime behavior.

```go
// orderdoesnotmatter.go — the source order of select branches does not decide which branch is chosen
package main

import "fmt"

func choose(a, b chan string) string {
	select {
	case v := <-b:
		return "b: " + v
	case v := <-a:
		return "a: " + v
	}
}

func main() {
	a := make(chan string, 1)
	b := make(chan string, 1)
	a <- "only a filled"

	fmt.Println(choose(a, b))
}
```

```
a: only a filled
```

**GC15.** — In this function, the `b` branch is written **first** in the source, the `a`
branch **second**. When run with only `a` full, the result is still `a: only a filled` —
showing that the order in the source does not change the outcome. Every `case` in a `select`
is weighed equally; whichever branch is ready is the one that runs. This raises the next
section's question: what would happen if two branches were ready **at the same time**, given
that order carries no priority?

This behavior comes from how `select` evaluates its branches. The order written in the
source is only the order the compiler reads the `case`s in; at runtime, `select` evaluates
**all** of the branches at once, at the moment it runs, finds the ones that are ready, and
then chooses among those. In an ordinary `if a != nil { ... } else if b != nil { ... }`
chain, the first branch whose condition holds would win — there, source order really is a
priority. `select` does not follow that model; this difference is one of the points most
often confused when learning `select`, because its syntax looks a lot like a `switch`, but
its evaluation rule is different.

#### Bounding Measurement: When Two Branches Are Ready at Once, Selection Is Random

Every measurement so far has set up situations where **at most one** branch is ready at any
given moment. What rule does the language apply when two branches are ready at the same
time? Go's own specification gives a clear answer to this question: when more than one
branch is ready at the same time, `select` chooses **uniformly at random** among them. This
is a **deliberate exception** to the determinism rule every measurement in this course has
rested on up to this lesson — and this is why this lesson does **not run** this case, only
models it.

```go
// modeledonly.go — when two branches are ready at once selection is random, this lesson does not run this, only shows it
package main

func chooseWhenBothReady(a, b chan string) string {
	select {
	case v := <-a:
		return "a: " + v
	case v := <-b:
		return "b: " + v
	}
}
```

**GC16.** — This function shows exactly the situation `select` faces when called with both
`a` and `b` full: the compiler makes no fixed preference between the two branches here, the
runtime chooses randomly again on every call. `chooseWhenBothReady` is never called anywhere
in this lesson — had it been called, it could have produced different results between two
runs, and this would deliberately break the "same output on every run" condition this
course's verification looks for. The function compiles only **because it exists**; the
source itself shows where the randomness shows up, not a run of it.

This randomness is not a shortcoming, it is a deliberate design decision. If `select` always
chose the first branch in the source between two ready branches, programmers would, over
time, start using this order like a priority rule, and this could lead to one of the channels
being systematically starved — if the same two branches stayed ready one after another, the
second branch might never run at all. Random selection removes this risk at the language's
own level: no branch can systematically get ahead just because it was written first in the
source.

This unmeasurability is the actual reason behind the previous section's finding that "source
order is not a priority." Had order carried a priority, which branch gets chosen when two
are ready at the same time would still be deterministic — the one written first would win.
Because order carries no weight at all, there is no real rule left to decide between two
branches that are both ready at once; the language fills this gap with randomness, a
different path from the "defined" class seen in the Go Fundamentals course: there, a
**fixed** value was placed in for something left unwritten, here, a value **drawn again
every time** is placed in for something left unwritten. This lesson does not carry this
distinction forward as an axis, it only notes it once.

Because this lesson never calls `chooseWhenBothReady`, the only way to show the difference
between two runs is to read the source. This is the one place, unlike every other lesson in
this course up through this one, where the claim "the same source can give a different result
on different runs" is proven **without running it**: Go's own specification guarantees that
the randomness exists, this lesson only shows, on the source itself, where that guarantee
takes effect.

#### select Is a Non-Blocking Control Tool

The `default` branch turns `select` into more than a polling tool, into a control tool that
keeps the flow moving without ever stopping it. This can be used to drain every ready value
in a channel until none are left — the moment the channel empties, the loop stops itself
without waiting.

This use separates two different jobs `select` does. In the first section's use, `select`
was making a **choice** among more than one channel — deciding which source was ready. Here,
`select` is used on a single channel, to **ask** whether that channel is ready; the presence
of the `default` branch is the language's way of saying "do not wait if it is not ready."
Both uses share the same syntax, but their purposes differ: one decides among more than one
source, the other reads a single source's instantaneous state.

```go
// nonblockingcontrol.go — select is used to keep the flow moving without waiting on a channel that is not ready
package main

import "fmt"

func tryToReceive(channel chan int) (int, bool) {
	select {
	case v := <-channel:
		return v, true
	default:
		return 0, false
	}
}

func main() {
	results := make(chan int, 3)
	results <- 10
	results <- 20

	total, count := 0, 0
	for {
		v, ok := tryToReceive(results)
		if !ok {
			break
		}
		total += v
		count++
	}
	fmt.Println("values received without blocking from the channel:", count, "total:", total)

	_, ok := tryToReceive(results)
	fmt.Println("attempt once the channel is empty                :", ok)
}
```

```
values received without blocking from the channel: 2 total: 30
attempt once the channel is empty                : false
```

**GC17.** — Only two values were placed on the `results` channel; the loop drains these two
with `tryToReceive`, and because `ok` returns `false` on the third attempt, the loop stops on
its own — no goroutine was waited for, no order was left to the scheduler. This setup lets a
channel be asked "what do you have for me right now"; if the channel itself is empty, the
asking side can move on without waiting. This is the general form of the technique used to
poll buffer capacity in the previous lesson: there, whether a single channel was full was
being asked, here the same technique is used to **drain** a channel.

The loop's stopping condition also stands out: the `for` loop does not stop based on a fixed
number, it stops based on the second value `tryToReceive` returns. How many times it will
loop cannot be read from the source code — no matter how many values were placed on the
`results` channel, the same loop runs and stops at exactly that many steps. This differs from
walking a slice with `range`: how many times `range` will loop can be read **in advance**
from the slice's length, while how many times this loop will run is only learned by running
it. This difference becomes even clearer when compared against the `range over channel`
behavior the closing lesson measures: `range` counts a channel's **closing** as its stopping
condition, while the loop here counts the channel's current **fullness** as its stopping
condition — both produce a finite loop, but based on a different signal.

This way of using it has a cost: `tryToReceive` does not **wait** for a goroutine to produce,
it only takes whatever is ready at that moment. If nothing has been written to the channel
yet, `tryToReceive` always returns `false` — even if the producing goroutine is about to send
a value a microsecond later. A non-blocking control tool loses waiting in return; which one
is needed, when `select` should be allowed to wait and when it should move on immediately, is
a design decision.

This danger becomes even clearer with the wrong use of putting a `default` branch inside a
loop: if the goal is to wait for a value to arrive, and the loop retries immediately every
time it falls to `default`, the program waits without ever blocking, by keeping the processor
continuously busy — this is `select`'s counterpart of the **busy waiting** pattern set up in
the context of locks and critical sections in the Operating System Concepts course. This
lesson does not set up that pattern: `tryToReceive` is called once, not waited on in a loop.
When waiting is actually needed, the right tool is a `select` with no `default` — then
`select` itself blocks until a branch is ready and does not keep the processor busy; this
would be what the first section's `choose` function looks like written without a `default`
branch.

#### Summary

- `select` deterministically chooses the one branch that is ready, among more than one
  channel operation, only when exactly **one** branch is ready.
- The branches' order in the source carries no priority; whichever branch is ready gets
  chosen no matter what order it was written in.
- When no branch is ready and there is a `default` branch, it runs immediately; `select`
  turns this way into a polling and control tool that never waits.
- When two branches are ready at the same time, selection is uniformly random; this is a
  deliberate exception to the determinism rule this course measures, and it is therefore not
  run, only modeled.
- `select`+`default` can be used to drain a channel of whatever values it holds, with no
  goroutine waited for, until none are left.
- A `select` with no `default` blocks when no branch is ready and does not keep the
  processor busy; this is the right tool when waiting is actually needed, not a loop with
  `default`.

#### Next Step

This lesson never touched on how a closed channel behaves inside `select`. A receive from a
closed channel makes a `select` branch ready right away, exactly like a receive from a full
channel — but the answer that comes back is not the measured value itself, it is the news
that the channel **will now stay empty**. The next lesson measures this last behavior:
closing a channel is a termination signal, and it measures how that signal shows itself in
both `range` and a direct receive.

### Channel Closing and Ranging

Source: https://academia.sh/en/courses/go-concurrency/closing-and-ranging

Closing a channel is a termination signal: range ends on a closed channel, and a direct receive from a closed channel returns the zero value and false. The answer given by four forms of receiving (open, closed-full, closed-empty, closed-empty again) measures this. The responsibility to close always belongs to the sender. Bounding measurement: sending to a closed channel and closing a channel a second time both panic; both are caught with recover and written only as panic=true/false.

The previous lesson measured `select`'s deterministic selection and its random exception,
but left one question open: how does a closed channel behave inside `select`? This lesson
opens with that question and moves on to the topic's last question — what **closing** a
channel announces, and whose responsibility that announcement is. This is the topic's fifth
and final lesson: how goroutines stop, what channels carry, what the buffer defers, and how
`select` chooses have all been measured; a single question remains — when does a
communication **end**, and who declares that it has ended.

#### A Closed Channel Is Immediately Ready Inside select

This was the question left at the end of the previous lesson: does a closed channel make a
`select` branch ready, even when it holds no values at all? The answer is yes — a receive
from a closed channel never blocks, and so that branch is always **ready** to be chosen.
What it carries back, though, is not a value, it is the news that the channel has ended.

```go
// selectclosed.go — a closed channel is immediately ready inside select, and returns the zero value and false
package main

import "fmt"

func main() {
	closed := make(chan int)
	close(closed)

	waiting := make(chan int)

	select {
	case v, ok := <-closed:
		fmt.Println("closed channel branch chosen, value=", v, "ok=", ok)
	case v, ok := <-waiting:
		fmt.Println("waiting channel branch chosen, value=", v, "ok=", ok)
	}
}
```

```
closed channel branch chosen, value= 0 ok= false
```

**GC18.** — `waiting` is never filled and never closed; this branch would never have become
ready. `closed`, on the other hand, is **always** ready from the moment `close` is called —
this is why `select` chooses the `closed` branch between these two without any hesitation at
all, the randomness from the previous lesson does not come into play here because only one
branch is genuinely ready. `v` stays at its zero value (`0`), and `ok` returns `false` — this
two-value form is the channels' counterpart of the same syntax seen in map access in the Go
Fundamentals course, but what it carries differs: in a map, `ok` says whether a key exists,
in a channel, it says whether the channel is **still open**.

This connects to the `nil` channel observation from the previous lesson: the way to make a
branch ineffective inside a `select` was to turn that branch's channel into `nil`, because a
`nil` channel was never ready. A closed channel sits at the exact opposite end — instead of
never being ready, it is **always** ready. Both are boundary cases, but in opposite
directions: one makes you wait forever, the other never makes you wait at all.

These two boundary cases can also be used together: a goroutine that has finished a job can
turn its own progress channel into `nil` so that branch is never chosen again, or it can
close a cancellation channel so that every waiter wakes up at the same time. Both of these
patterns are the subject of the Concurrency Patterns topic; only the boundary cases
themselves were measured here.

#### Four Forms of Receiving

What closing carries changes depending on which state the channel is in at that moment.
Four states are distinguished from one another: a value is received while the channel is
open, a value still left in the buffer is received after the channel closes, a receive
happens after the buffer has also emptied, and this last state repeats.

```go
// fourreceives.go — the four forms of receiving from a closed channel: open, closed-full, closed-empty, range
package main

import "fmt"

func main() {
	channel := make(chan int, 2)
	channel <- 10
	channel <- 20

	v1, ok1 := <-channel
	fmt.Println("received while open       :", v1, ok1)

	close(channel)

	v2, ok2 := <-channel
	fmt.Println("closed, buffer had a value:", v2, ok2)

	v3, ok3 := <-channel
	fmt.Println("closed and empty          :", v3, ok3)

	v4, ok4 := <-channel
	fmt.Println("closed and empty again    :", v4, ok4)

	total, stepCount := 0, 0
	channel2 := make(chan int, 3)
	channel2 <- 1
	channel2 <- 2
	channel2 <- 3
	close(channel2)
	for value := range channel2 {
		total += value
		stepCount++
	}
	fmt.Println("range total and steps     :", total, stepCount)
}
```

```
received while open       : 10 true
closed, buffer had a value: 20 true
closed and empty          : 0 false
closed and empty again    : 0 false
range total and steps     : 6 3
```

**GC19.** — The first receive happens before the channel is closed, and returns `true` like
an ordinary receive. When `close(channel)` is called, a value (`20`) is still sitting in the
buffer; closing **does not erase** this value — the second receive gives it back with `true`.
The third receive, after the buffer has emptied, now gives the zero value (`0`) and `false`;
the fourth receive repeats the same answer, because a receive from a closed and empty channel
gives this answer **forever**, it never blocks again. `range` turns this behavior into a
loop: once the channel closes and its buffer empties, the loop ends on its own, there is no
need to write a `break` — the channels' counterpart of the "`break` that never needs to be
written" pattern seen in the Control Flow lesson in the Go Fundamentals course.

These four answers show exactly what closing announces: closing **does not invalidate the
values that already exist**, it only announces that **no new value will arrive**. Values
waiting in the buffer can still be received normally after closing; the signal only kicks in
once the buffer is used up.

It matters that these four states were measured in sequence: the measurement is built up
step by step, inside a single goroutine, with no goroutine started. How many values are left
in the buffer at the moment of closing is known in advance (`channel <- 10` and `channel <-
20` placed two values, only one was received), which is why it is certain from the start
that the "closed but buffer still has a value" state will appear exactly once. Had the same
measurement been done on an unbuffered channel, the second state (closed but the buffer still
has a value) would never have happened at all — an unbuffered channel cannot have a value
waiting at the moment of closing, because every send only completed once a receiver was
ready.

#### The Responsibility to Close Belongs to the Sender

The authority to close a channel can technically be called by either the sender or the
receiver — `close` can be run by any side that holds a channel value. But there is only one
**correct** use of this authority: only the sending side should close it, because only the
sending side can have the information that "I have no more values left to send." A receiver
closing the channel carries the risk of closing it while the sender still has a value it
plans to send — and a send to a closed channel panics, the subject of this lesson's final
section.

This rule is not written into the channel's own type. Even a parameter restricted with
`chan<- int` can call `close` — the direction restriction only separated sending from
receiving, closing is a separate operation from both and is defined in both directions. The
source of the rule is not the language, it is **information**: only the sender has any
possibility of knowing when it will finish sending, the receiver can never have that
information at all. A receiver can never know for certain, from the source code, whether it
will be sent to again.

Never closing at all also has a cost. A `range` waiting on a channel that is never closed
never stops, for as long as the channel stays unclosed — this is the "never stopped" class
measured in the course's first lesson, showing up here again. One of the three examples that
fell into that class in that lesson was exactly this: a goroutine ranging over a channel that
never closes never finishes on its own, even if the rest of the program does.

```go
// neverclosedrisk.go — range on a channel that never closes never stops, the leak shows as a boolean
package main

import (
	"fmt"
	"runtime"
)

func main() {
	baseline := runtime.NumGoroutine()

	neverClosed := make(chan int)
	started := make(chan struct{})
	go func() {
		close(started)
		for range neverClosed {
		}
	}()
	<-started

	fmt.Println("is there more than the baseline goroutine count:", runtime.NumGoroutine() > baseline)
}
```

```
is there more than the baseline goroutine count: true
```

**GC20.** — The `started` channel exists only to confirm the goroutine has started; what is
actually measured is that the `range` over `neverClosed` never finishes. Because
`neverClosed` is never closed, this goroutine stays in memory until the program ends. The
number `runtime.NumGoroutine` gives is never printed anywhere, it is only compared against
the baseline and reduced to `true`/`false` — the rule set up in the course's first lesson
continues here exactly the same. This measurement shows that closing is **not an optional
nicety**, it is a requirement whenever a waiting party exists.

This measurement's correctness rests on two things. First, the `<-started` receive
guarantees the goroutine has reached at least the `close(started)` line — which shows the
goroutine has not returned yet, and is therefore still countable. Second, the goroutine's
body has exactly one line after `close(started)` (`for range neverClosed`), and this line
never finishes, because `neverClosed` is never closed; there is no possibility the goroutine
has returned after the `<-started` receive. Together, these two observations guarantee that
`runtime.NumGoroutine` gives the same answer (`true`) on every run — here too, no order was
left to the scheduler.

#### Bounding Measurement: Sending to a Closed Channel and Closing It Twice Both Panic

Closing has rules of its own, and the language does not stay silent when these rules are
broken. Trying to send to a closed channel panics; trying to close the same channel a second
time also panics. Both of these behaviors are caught with `recover` and written only as
`panic=true`/`panic=false` — the stack trace and error text are not printed.

```go
// closeresponsibility.go — sending to a closed channel and closing it a second time both panic, caught with recover
package main

import "fmt"

func doesItPanic(f func()) (p bool) {
	defer func() {
		if recover() != nil {
			p = true
		}
	}()
	f()
	return false
}

func main() {
	channel := make(chan int, 1)
	close(channel)

	sendPanicked := doesItPanic(func() { channel <- 1 })
	fmt.Println("send to closed channel panic=", sendPanicked)

	secondClosePanicked := doesItPanic(func() { close(channel) })
	fmt.Println("closing the channel a second time panic=", secondClosePanicked)
}
```

```
send to closed channel panic= true
closing the channel a second time panic= true
```

**GC21.** — `doesItPanic` runs the function it is given inside a `defer`/`recover` pair,
and returns only whether it panicked or not as a `bool` — the same panic/recover pattern
established in the Go Fundamentals course. The function inside `defer` calls `recover()`; if
there is a panic, `recover` stops it, returns its value, and sets `p` to `true`, if there is
no panic, `recover` returns `nil` and `p`, as the named return value, stays at its initial
`false`. Even though `channel` could hold a value (capacity `1`, but empty here), every send
panics once it has been closed; this does not contradict the third section's "closing does
not erase values" observation — closing only forbids **new** sends, not reading what already
exists. The second close panics in the same way, because `close` changes a channel's state
**irreversibly**; there is no operation defined in the language for "closing again" a channel
that is already closed.

Together, these two panics show why the responsibility to close has to sit with a single
side. If two separate goroutines try to close the same channel, the second one panics. If two
separate goroutines are sending to the same channel and one of them closes it, the other's
next send panics. Without an agreement on **whose job** closing is, more than one goroutine
cannot share the same channel — this problem itself is the subject of the Concurrency
Patterns topic.

There is also a difference between these two panics. A panic from sending to a closed
channel is triggered at the **same** place in the source code, under the same conditions, on
every run — if there is a send after `close`, the panic is certain. A panic from closing a
second time is usually the result of a race, where two separate goroutines try to close the
same channel; which goroutine closes it **first** may not be deterministic, but that at least
one of them will panic is certain. This lesson sets up the second case, too, inside a single
goroutine, with two `close` calls in a row, deterministically — showing the same result
without running a real race.

A panic here is not a display of an error, it is a display of a **contract violation**.
Sending to a closed channel, or closing a channel a second time, is not a runtime error, it
is proof that the program's own logic has tracked a channel's state incorrectly. Even though
both of these can be caught with `recover`, the course's rule still holds: instead of
printing the panic text, only `true`/`false` is written, because the panic text is a detail
tied to the version.

#### Summary

- A closed channel is always ready inside `select`; what it carries back is the zero value
  and `false`, not data but news of termination.
- Closing does not erase values that already exist: values waiting in the buffer can still
  be received normally after closing, and only once the buffer is used up do the zero value
  and `false` begin.
- `range` stops on its own once the channel closes and its buffer empties; there is no need
  to write a `break` as well.
- The responsibility to close belongs to the sender; a `range` waiting on a channel that is
  never closed never stops, and this is a measurable goroutine leak.
- Sending to a closed channel and closing a channel a second time both panic; both are
  violations of an irreversible state change.
- The authority to close sits in information, not in the channel's type: only the sender can
  ever have any possibility of knowing when it will finish sending, the receiver never does.

#### Next Step

This topic's five lessons measured communication between one goroutine and one other
goroutine: one channel, two sides, one sender, one receiver. Each lesson measured one
direction of this pairwise relationship — the goroutine itself, the channel's two jobs, the
buffer's deferral, `select`'s selection, closing's signal — and each one added a piece of
answer to the course's opening question, how a goroutine will stop.

Closing is a termination signal, and the responsibility belongs to the sender — but if there
is more than one sender, or more than one receiver, how does this responsibility get shared?
If more than one goroutine tries to close the same channel at once, the second one panics;
this lesson's final measurement showed exactly that. The next topic looks at the patterns
that use this signal **among a large number of goroutines**: worker pools, fan-out and
fan-in, pipelines — and in each of them, how the responsibility to close is shared among how
many sides.

## Concurrency Patterns

Distributing work with a fixed number of workers wired to job and result channels, a job being fanned out to many and the results gathered back, each stage's own closing responsibility in stream processing, context cancellation and carrying request-scoped values, and where timeouts and deadlines do and do not prevent a resource leak.

### Worker Pools

Source: https://academia.sh/en/courses/go-concurrency/worker-pools

A fixed number of workers reads from a shared job channel; when the channel closes, all of them stop for the same reason — what they waited for arrived. When five jobs are distributed to eight workers, the minimum count of workers that stopped without ever taking a job is derived from the total jobs taken and the total reporting worker count. Limiting measurement: raising the worker count does not split the existing work, it only leaves more workers idle; in a job channel that is never closed, workers never stop at all.

The previous topic measured that closing is a termination signal, and that the responsibility
for sending this signal stays with the sender — a receive from a closed channel returned the
zero value and `false`, while ranging over the channel ended. That measurement worked with a
single receiver. This lesson looks at the first pattern that uses the same signal across
**many** goroutines at once: a fixed number of workers share a single job channel, and when the
channel closes, they do not all stop at the same moment, but they do stop for the same
**reason**.

The worker pool pattern itself is simple: a job channel, a result channel, and a fixed number of
worker goroutines. Each worker reads from the job channel, processes the job, writes the result
to the result channel; the read loop ends when the job channel closes. What is new is that the
termination signal a single receiver saw in the previous lesson now reaches **more than one
goroutine at the same time** — and each one reacts to that signal independently. This lesson's
question: when a fixed number of workers see this shared signal, how many are left having done
which amount of work, and what happens once the worker count exceeds the job count?

The pool itself rests on a single idea: if the worker count is kept fixed, resource usage stays
fixed too — no matter how many jobs arrive, the number of goroutines running at the same time
never changes. This is the exact opposite design decision from starting a separate goroutine for
every job (an alternative not seen in the previous lesson, but one that comes to mind): so that
the goroutine count does not grow without bound as the job count grows uncontrolled, the worker
count is fixed from the start, and the job channel acts as a buffer between this fixed count and
the growing set of jobs. Measuring how this buffer is drained — not which worker took how much,
but whether all of them stopped at the same moment or for different reasons — is this lesson's
subject.

#### A Fixed Number of Workers, a Shared Queue

The measurement below distributes five jobs to eight workers. Every worker reads from the same
`jobs` channel, writes the value it squared to the `results` channel, and once its own share of
work is done — that is, once the `jobs` channel closes and its loop ends — sends a report. The
report carries a single reason: **what it waited for arrived**. No worker stops by its own
decision; all of them are waiting for the same event, the channel closing.

```go
// pool.go — a fixed number of workers reads from a shared job channel; when it closes, all stop for the same reason
package main

import (
	"fmt"
	"sort"
	"sync"
)

func worker(id int, jobs <-chan int, results chan<- int, reports chan<- string, group *sync.WaitGroup) {
	defer group.Done()
	for job := range jobs {
		results <- job * job
	}
	// The wait group (sync.WaitGroup) is used here only as a tool; its own
	// lesson comes later in the course.
	reports <- "what it waited for arrived"
}

func main() {
	const jobCount = 5
	const workerCount = 8

	jobs := make(chan int, jobCount)
	results := make(chan int, jobCount)
	reports := make(chan string, workerCount)
	var group sync.WaitGroup

	for id := 1; id <= workerCount; id++ {
		group.Add(1)
		go worker(id, jobs, results, reports, &group)
	}

	for i := 1; i <= jobCount; i++ {
		jobs <- i
	}
	close(jobs)

	group.Wait()
	close(results)
	close(reports)

	fmt.Println("-- 5 jobs, 8 workers: closed job channel --")
	reportCount := 0
	allSameReason := true
	for r := range reports {
		reportCount++
		if r != "what it waited for arrived" {
			allSameReason = false
		}
	}

	resultList := make([]int, 0, jobCount)
	for s := range results {
		resultList = append(resultList, s)
	}
	sort.Ints(resultList)
	total := 0
	for _, s := range resultList {
		total += s
	}

	fmt.Printf("total jobs taken (sum across all workers): %d\n", len(resultList))
	fmt.Printf("total reporting worker count: %d\n", reportCount)
	fmt.Printf("did all stop for the same reason (what it waited for arrived): %v\n", allSameReason)
	fmt.Printf("minimum idle worker count (%d - %d): %d\n", workerCount, jobCount, workerCount-jobCount)
	fmt.Printf("collected result count: %d, total: %d\n", len(resultList), total)
}
```

```
-- 5 jobs, 8 workers: closed job channel --
total jobs taken (sum across all workers): 5
total reporting worker count: 8
did all stop for the same reason (what it waited for arrived): true
minimum idle worker count (8 - 5): 3
collected result count: 5, total: 55
```

The measurement itself does not leave order to the scheduler either. The `results` and
`reports` channels are not closed **before** `group.Wait()` returns — the wait group
(`sync.WaitGroup`) blocks `Wait()` until every worker has called `group.Done()`, and none of the
eight workers can reach `group.Done()` without first sending its own report (the line order at
the end of the helper function guarantees this). Without this ordering, the main goroutine could
close the `reports` channel before any report had arrived, and the `for r := range reports` loop
could see fewer than eight reports — a number that would change from run to run. What
`group.Wait()` does here is exactly this: guarantee, through a channel, without waiting any
duration at all, that all eight workers have finished their work.

The channels being opened as buffered is not random either. `jobs` is opened with exactly
`jobCount` capacity, so the main goroutine can send all the jobs even before any worker has
started reading — sending does not wait for workers to start reading. `reports` is also opened
with `workerCount` capacity, because all eight of the eight workers will want to send their
report, and on an unbuffered channel this send would wait for the main goroutine to read. The
main goroutine only reads the reports after `group.Wait()` returns; since `group.Done()` is
deferred in every worker, it runs **after** the report is sent. On an unbuffered `reports`
channel, the first worker would block on its send, would never reach its `Done`, `Wait` would
never return, and the program would deadlock entirely. The buffers here are not a speed
optimization, they are a design decision that keeps the code's flow simple.

**CP1.** There is a deliberate choice here: the measurement never prints which worker took which
job. Eight workers race to read from the same channel, and the winner of this race cannot be
known before running it — in one run the third worker might take two jobs, in another it might
take none at all. **Total jobs taken** (the count of squares written to the `results` channel)
and **total reporting worker count** (eight) never change: the first is the direct result of how
many jobs were sent, the second of how many workers were started, independent of which worker
took what. The lesson measures only what does not change.

**CP2.** The answer to "did all stop for the same reason" is fixed for the same reason: no
matter how many jobs a given worker takes, when the `jobs` channel closes, **every** worker's
`for job := range jobs` loop ends and a report is sent. The **reason** for stopping is
independent of the job distribution; only the **moment** of stopping varies from worker to
worker, and this lesson measures the reason, not the moment.

**CP3.** What the measurement itself does not print matters just as much: no line says "worker 3
took 2 jobs." The reason is that this piece of information would **not stay the same** from one
run to the next — eight goroutines listen to the same `jobs` channel at the same time, and which
one catches which send depends on Go's own runtime scheduler. This is not a defect, it is the
natural result of a shared channel; but `kod_dogrula.py` compares the run's output against the
lesson's fixed expected-output block character for character, so printing a detail at this level
would break the lesson against its own verification tool. Instead, the measurement prints only
the two totals that stay **unchanged** from run to run (jobs taken, reporting workers) and
**derives** the third number (idle workers) from them — a computed number, not an observed one.
This is why the `id` parameter `worker` takes is never read inside its body: what is measured is
not the identity itself, but how many workers exist and in which class they stop.

#### Raising the Worker Count Does Not Split the Work

**CP4.** The last line is one of the course's clearest limiting observations: eight workers were
started but there were only five jobs. Since the count of squares written to the `results`
channel equals five — each worker may have taken as few as zero jobs, and the total is five —
**at most five** workers can have taken a job; the remaining **at least three** must have
stopped without ever taking one. This result is an **arithmetic necessity** following from two
directly measured numbers (total jobs taken, total worker count); there is no need at all to
observe which three workers sat idle, because how many did is already given by the totals. The
same logic holds for every `jobCount < workerCount` case: if the worker count were raised from
eight to nine, the lower bound on idle workers would also rise from three to four — as long as
adding workers does not change the amount of work, the lower bound on idle worker count **grows
together** with the worker count, it is not inversely related to it.

The reverse case comes from the same arithmetic: if `jobCount` were greater than `workerCount`
(twenty jobs, eight workers, say), the lower bound on idle workers would drop to zero — all eight
of the eight workers would be forced to take at least one job, because all twenty jobs must be
processed somewhere and there are only eight channel ends to process them. Sitting idle only
happens when the job count is **less** than the worker count; the pool itself handles both
directions through the same code path, with no special case added at all.

This is where it becomes visible that raising the worker count **does not split the existing
work**: the five jobs in the job channel can only be taken five times, no matter how many
workers are listening. Adding an eighth worker does not cause the fifth job to be broken into
smaller pieces — it only leaves more workers waiting in line, with empty hands once the channel
closes. If a job itself is not a divisible unit (like the `job * job` above, an atomic step
processing a single integer), raising the worker count above the job count buys not parallelism
but only idle workers. In a real system this is usually harmless — an idle goroutine consumes no
resources, it only listens on a channel — but for a designer planning to size the pool to the
workload, this measurement says: raising the worker count does not guarantee a speedup unless
the work itself can be **split** into smaller pieces. This pool arrangement is not compared here
against the executor concept — the pool comparison from **M07/K04 Concurrency and Performance**
is not repeated here; what is measured is only Go's own channel-based mechanism.

Sizing the pool therefore requires looking at the work's **own shape**: if a job is an
indivisible step like squaring a single integer, the benefit flattens out the moment the worker
count passes the job count. If a job carries a larger set (the elements of a slice, say), sending
that set's sub-parts separately to the same job channel makes the worker count meaningful again
— but that is a decision about how the work itself is split, not something the pool resolves on
its own.

#### An Unclosed Queue: Workers Never Stop

The measurement above closed the `jobs` channel with `close(jobs)` and made all of them stop.
What would happen if this closing line were forgotten? The measurement below builds the same
arrangement but never closes the job channel at all.

```go
// leak.go — if the job channel is never closed, workers never stop; the leak is measured as a boolean
package main

import (
	"fmt"
	"runtime"
)

func leakyWorker(id int, jobs <-chan int, results chan<- int) {
	for job := range jobs {
		results <- job * job
	}
	// This line is never reached: the jobs channel never closes.
}

func main() {
	base := runtime.NumGoroutine()

	const workerCount = 4
	jobs := make(chan int) // never closed
	results := make(chan int, workerCount)

	for id := 1; id <= workerCount; id++ {
		go leakyWorker(id, jobs, results)
	}

	fmt.Println("-- job channel never closed --")
	fmt.Printf("started worker count: %d\n", workerCount)
	fmt.Printf("did a non-stopping worker remain: %v\n", runtime.NumGoroutine() > base)
}
```

```
-- job channel never closed --
started worker count: 4
did a non-stopping worker remain: true
```

**CP5.** Four workers were started and none of them ever took a job — because no value was ever
sent on the `jobs` channel. But that is not the real observation: all four of the four workers
wait **forever** on the `for job := range jobs` line, because the channel neither carries a value
nor closes. This is the worker pool arrangement's counterpart to the previous lesson's "stopping
on its own is also a contract" observation: a worker stopping is not a decision of its own body,
it is a decision made by **whoever manages the job channel**. `runtime.NumGoroutine()` staying
above the base count is the only proof of this — the program terminates normally with no error
and no warning at all, even though four goroutines are left hanging in the background. How many
goroutines remain is not printed here, only the fact that some remain at all.

This measurement's correctness does not depend on run timing: the moment a `go` statement runs,
the new goroutine is **registered** with the runtime, even before it has run any code at all —
this is why, by the time the loop ends and `runtime.NumGoroutine()` is called, all four workers
have already entered the count. The measurement therefore does not need to wait for the four
workers to actually become blocked — the registration itself is enough, because what matters is
not what the workers are doing right now, but **whether they exist at all**.

This single leak might not cause an observable problem in a short-lived program — four
goroutines are cleaned up along with the memory of a process that is ending anyway. The real
risk is in the pattern **repeating**: if a request handler builds its own pool on every call and
forgets to close the job channel, the leaked worker count grows together with incoming requests.
This lesson measures a single leak, not a multiplied one — but the boolean itself is already
warning enough: a `true` answer is the sign of a design in which the channel will never close, no
matter how many goroutines pile up.

#### Summary

- A fixed number of workers reads from a shared job channel; when the channel closes, all of
  them stop for the **same reason** (what it waited for arrived), independent of which worker
  took which job.
- Total jobs taken and total reporting worker count are two fixed values independent of job
  distribution; from the difference between the two, the minimum count of workers that took no
  job at all is computed.
- Raising the worker count above the job count does not split the existing work; it only raises
  the idle worker count, because each job is an indivisible unit taken exactly once.
- If the job channel is never closed, workers wait forever; the proof of the leak is measured
  only as a boolean — how many goroutines remain is never printed.
- The responsibility for closing stays with the sender here too — in a worker pool, the sender
  is the side that distributes the work.

#### Next Step

In this lesson, every worker read from the **same** channel, and the channel closing reached all
of them at the same time. But a single source is not always enough: sometimes a job comes from a
stream that needs to be consumed faster than a single producer could produce it, and that stream
itself must be **fanned out** to more than one consumer. The next lesson measures this fan-out —
a single source spread across many consumers, with the results gathered back into a single
channel — and this time the question is different from the worker pool's: with more than one
producer, which one holds the responsibility for closing the gathering channel?

### Fan-Out and Fan-In

Source: https://academia.sh/en/courses/go-concurrency/fan-out-and-fan-in

When a source is fanned out to a fixed number of consumers (fan-out), the results merge back into a single fan-in channel (fan-in); the fan-in side's reason for stopping depends on a wait group closing the fan-in channel once all producers finish. Limiting measurement: the question of who closes the fan-in channel has exactly one answer — no producer can safely close it on its own (of three producers that try, two panic while one succeeds), and if it is never closed, the fan-in side never stops.

The previous lesson measured a setup where a fixed number of workers listened to the **same**
job channel: a single source, many consumers, and a termination signal that reached all of them
through a single close. But work does not always arrive at a rate a single worker could keep up
with on its own. Sometimes the values being produced themselves must be **distributed** across
more than one processor — this is **fan-out** — and the results of that distributed work must be
**merged** back into a single stream — this is **fan-in**. The previous lesson's "who closes the
channel" question applies here too, but the answer sits in a different place: in the worker pool,
the closing side was the one **distributing** the work, while here, **more than one** side writes
to the channel that needs closing — and this, for the first time, turns "who closes it" into a
real problem.

Fan-out and fan-in cover two separate directions. Fan-out is a production source (a queue, a
channel) opening out to more than one processor — nearly the same mechanism as the previous
lesson's job channel, because a fixed number of goroutines reading from a shared channel is
already a fan-out. Fan-in works in the opposite direction: more than one source merging into a
**single** channel. When the two directions are set up together, a new question arises — in
fan-out the closing side is clear (the single side managing the source), but in fan-in, however
many sides write to the channel that needs closing, that many sides carry the risk of the closing
responsibility being split among them.

#### A Source Fanned Out to Many, Results Fanned Into One Channel

The measurement below fans six values out to three producer goroutines; each producer squares
the values falling to its share and writes them to a single `fanin` channel. Who will close the
fan-in channel is already resolved here: a separate goroutine waits for a wait group
(`sync.WaitGroup`) to report that all producers are finished, and only then closes `fanin`. Sole
ownership is established here **not in one producer, but outside all the producers**.

```go
// fanoutfanin.go — a source fans out to three producers, results fan in to one channel; the wait group closes the fan-in channel
package main

import (
	"fmt"
	"sort"
	"sync"
)

func producer(input <-chan int, fanin chan<- int, group *sync.WaitGroup) {
	defer group.Done()
	for value := range input {
		fanin <- value * value
	}
}

func main() {
	const jobCount = 6
	const producerCount = 3

	input := make(chan int, jobCount)
	fanin := make(chan int, jobCount)
	var group sync.WaitGroup

	for id := 1; id <= producerCount; id++ {
		group.Add(1)
		go producer(input, fanin, &group)
	}

	for i := 1; i <= jobCount; i++ {
		input <- i
	}
	close(input)

	// Fan-in: closes the fanin channel ITSELF once all producers finish;
	// no producer closes it on its own.
	go func() {
		group.Wait()
		close(fanin)
	}()

	fmt.Println("-- source fanned out to 3 producers, results fanned in to one channel --")
	results := make([]int, 0, jobCount)
	for s := range fanin {
		results = append(results, s)
	}
	sort.Ints(results)

	total := 0
	for _, s := range results {
		total += s
	}
	fmt.Printf("fanned-out job count: %d\n", jobCount)
	fmt.Printf("collected result count: %d, total: %d\n", len(results), total)
	fmt.Println("fan-in's reason for stopping: what it waited for arrived (the fan-in channel closed)")
}
```

```
-- source fanned out to 3 producers, results fanned in to one channel --
fanned-out job count: 6
collected result count: 6, total: 91
fan-in's reason for stopping: what it waited for arrived (the fan-in channel closed)
```

**CP6.** The `input` channel plays the same role as the previous lesson's `jobs` — a queue shared
among three producers. What is different is the `fanin` channel: all three producers write to it
**at the same time**, not a single worker. The result count reaching six and the total reaching
ninety-one (the squares `1+4+9+16+25+36`) is independent of which producer processed which value;
this is exactly why sorting (`sort.Ints`) is needed — the order in which the three producers write
to the fan-in channel can vary from run to run, but the set itself does not change.

In this measurement, the `producer` function has almost the same body as the previous lesson's
`worker`: both read from an input channel, both write to an output channel. The difference is not
in the function's own code, it is in the context it is called in — `worker` was set up so a fixed
number of consumers would share the same job, while `producer` runs the same function this time
in the context of **more than one writer writing to the same destination**. The pattern's name
changes (from worker pool to fan-out), but the underlying channel mechanism stays the same; only
where the closing responsibility sits changes.

**CP7.** The closing goroutine here is a third party: neither one of the producers, nor the
fan-in side itself. `group.Wait()` does not return until all three of the three producers have
reached `group.Done()`, and only then does `close(fanin)` run — this guarantees that the
**last** write to the fan-in channel has already happened. Both the `input` and `fanin` channels
are opened buffered with `jobCount` capacity, for the same reason as the previous lesson: so the
producers can take their share and write without being blocked. This third-party approach raises
the next section's question: what if this separate goroutine did not exist at all, and each
producer tried to close the channel itself once its own work was done?

#### Who Closes the Fan-In Channel

Closing a channel in Go is a **one-time** operation: closing the same channel twice panics. If
all three producers share the same channel and each one says "I am done, let me close the
channel" once it finishes its own work, the first one to close succeeds, but every closing
attempt after that panics. The measurement below sets exactly this up: each producer first sends
its own value, then (after a wait point makes sure all producers have finished sending) tries
`close(fanin)` on its own.

```go
// naive.go — if every producer tries to close on its own, all but the first panic
package main

import (
	"fmt"
	"sync"
)

func naiveProducer(id int, fanin chan int, sent *sync.WaitGroup, result chan<- bool, finished *sync.WaitGroup) {
	defer finished.Done()
	fanin <- id
	sent.Done()
	sent.Wait() // wait until every producer has sent its own value
	panicked := false
	func() {
		defer func() {
			if r := recover(); r != nil {
				panicked = true
			}
		}()
		close(fanin)
	}()
	result <- panicked
}

func main() {
	const producerCount = 3
	fanin := make(chan int, producerCount)
	result := make(chan bool, producerCount)
	var sent sync.WaitGroup
	var finished sync.WaitGroup
	sent.Add(producerCount)
	finished.Add(producerCount)

	for id := 1; id <= producerCount; id++ {
		go naiveProducer(id, fanin, &sent, result, &finished)
	}
	finished.Wait()
	close(result)

	succeeded, panicking := 0, 0
	for panicked := range result {
		if panicked {
			panicking++
		} else {
			succeeded++
		}
	}
	fmt.Println("-- each producer sends its own value first, then tries to close --")
	fmt.Printf("producers that closed successfully: %d / %d\n", succeeded, producerCount)
	fmt.Printf("producers that panicked: %d / %d\n", panicking, producerCount)
}
```

```
-- each producer sends its own value first, then tries to close --
producers that closed successfully: 1 / 3
producers that panicked: 2 / 3
```

**CP8.** The result is single-answered in a not-so-surprising way: on every run, exactly **one**
producer closes successfully, and the remaining **two** panic. Which producer succeeds can
change from run to run (all three start trying to close at the same time), but **how many**
succeed never changes — Go's runtime protects a channel's closing with an internal lock, so even
if more than one goroutine calls `close` at the same moment, only one of them really closes the
channel, and the rest find it already closed and panic. This is why the lesson prints not
**which** one succeeded, but **how many** did.

This internal lock means that `close` itself is an **atomic** operation: whether a channel is
"closed or not" always has a single, sequential history, even when more than one goroutine tries
to ask and change it at the same instant. Even if three goroutines really call `close(fanin)` at
the same moment, the runtime processes them in some invisible order — the first one processed
succeeds, every one after it finds an already-closed channel and panics. This behavior is very
different from writing to a shared variable without protection: if two goroutines write to a
shared counter at the same time, the result is an undefined race condition (not measured here,
only established as a concept in **M01/K05 Operating System Concepts**), but closing a channel is
an operation the language itself guarantees, not open to a race — whoever wins, exactly one
winner comes out.

**CP9.** The second synchronization point (`sent`) is a deliberate design decision here: the
measurement makes sure every producer **sends its own value first**, then tries to close. Without
this separation, one producer could close the channel before another had sent its own value, and
that producer's send (`fanin <- id`) would end up not **before the close line**, but as a
**panic** — which would confuse which line panicked for which reason. `sent.Wait()`, just like
`group.Wait()` seen in the previous lesson, separates the two phases (sending, closing) without
leaving the order to the scheduler.

This separation itself hides one observation: a panic about a closed channel can actually be
triggered in **two** separate forms — closing a closed channel again (`close of closed channel`)
and sending to a closed channel (`send on closed channel`). Without the `sent` barrier above, a
producer might close the channel before another had sent its own value (`fanin <- id`), and since
that send line sits **outside** its own `recover` block, the panic would go uncaught and the
program would crash — the lesson would stop before ever reaching `kod_dogrula.py`. The
`sent.Wait()` barrier prevents exactly this: no producer tries to close until it is certain all
sends have finished, which is why the observed panic type is always the same (closing an
already-closed channel), never the other one.

This measurement shows **why** the correct arrangement in the previous section is correct: the
responsibility for closing the fan-in channel does not belong to **any** of the producers,
because none of them knows when the others will finish. The responsibility can only be given to
a third party that knows when all the producers have finished — a separate goroutine waiting on
a wait group.

#### An Unclosed Fan-In Channel

The previous two setups showed two separate outcomes of splitting the closing responsibility
between **zero** and **more than one** side: in the first setup, responsibility belonged to
exactly one side, and everything ended silently; in the second setup, responsibility (mistakenly)
belonged to three sides, and two panics resulted. A third possibility remains: what happens if
responsibility is given to **no one at all**? If the third-party goroutine (the separate
goroutine above, waiting on the wait group and closing the fan-in channel) is never set up at
all, the fan-in channel never closes. The measurement below sets this up in its plainest form: a
fan-in goroutine ranges over the `fanin` channel, but there is no one at all to close the channel.

```go
// leak.go — if the fan-in channel is never closed, the fan-in side never stops
package main

import (
	"fmt"
	"runtime"
)

func main() {
	base := runtime.NumGoroutine()

	fanin := make(chan int)
	go func() {
		// Since no one closes the fan-in channel, this range will never end.
		for range fanin {
		}
	}()

	fmt.Println("-- fan-in channel never closed --")
	fmt.Printf("did a non-stopping fan-in side remain: %v\n", runtime.NumGoroutine() > base)
}
```

```
-- fan-in channel never closed --
did a non-stopping fan-in side remain: true
```

**CP10.** This is the fan-in side's mirror image of the previous lesson's unclosed job channel
measurement: there, the workers waited forever; here, a single fan-in side waits forever. The
difference is **where** the leak sits: in the worker pool, the leaking side was many consumers;
here, the leaking side is a single fan-in goroutine — but in both, the cause is the same, ranging
over a channel that no one closes. Since the fan-in side is itself a goroutine too, its leak is
measured not by a different tool than the worker leak, but with the exact same
`runtime.NumGoroutine()` comparison.

What these three setups have in common is that none of them solves the problem **at its source**:
even the correct setup assumes that all producers will **eventually** finish. If one of the
producers never finishes its own work (say, it enters an infinite loop), `group.Wait()` never
returns either, and the fan-in channel again never closes — this setup gives the closing
responsibility to the right side, but that side itself still depends on a condition. **Bounding**
the possibility of a producer never finishing its own work (with a fixed duration or a signal
coming from outside) is not this lesson's subject; the next lesson, measuring cancellation
propagation with the context package, takes up that question.

The three measurements together complete this topic's thesis: a correctly built fan-in
(responsibility gathered in one side) stopped deterministically and silently; an incorrectly
built fan-in (everyone trying to close on their own) stopped deterministically but **noisily** —
with panics; and a never-built fan-in (no one closing) never stopped at all. All three are
different answers to the same question: the responsibility for closing the fan-in channel must
belong to exactly **one** side, neither fewer nor more. This principle is a generalization of the
same principle behind the worker pool's single closing side (the side distributing the work): no
matter how many sides write to a channel, closing always stays a separate, singular
responsibility.

#### Summary

- When a source is fanned out to a fixed number of producers (fan-out), the results merge back
  into a single fan-in channel (fan-in); the collected result count and its total are two fixed
  values independent of the producer count.
- The responsibility for closing the fan-in channel belongs to none of the producers — a third
  party closes it, one that a wait group tells once all producers have finished.
- If every producer tries to close on its own, exactly one succeeds and the rest panic; which
  producer succeeds can change, but how many succeed never does.
- If the fan-in channel is never closed, the fan-in side waits forever; the leak is again
  measured only as a boolean.
- The closing responsibility must belong to exactly one side: zero sides leads to a leak, more
  than one leads to a panic.
- Closing a channel is an atomic operation the language itself guarantees; even if more than one
  goroutine calls `close` at the same moment, exactly one succeeds, the rest panic.

#### Next Step

In this lesson, a source fanned out to many and the results fanned back into a single channel —
but the flow was a single step: each producer took the input, squared it, wrote the result. Real
processing usually passes through more than one step, and each step has its own speed, its own
closing responsibility. The next lesson chains these steps — building a pipeline out of stages,
with each stage closing its own output channel on its own. This time the question is not who
holds a single closing responsibility: if a stage in the chain stops early, what happens to the
stages ahead of it?

### Pipelines

Source: https://academia.sh/en/courses/go-concurrency/pipelines

In a pipeline arrangement, each stage opens its own output channel and closes it itself; five values passing through a generation stage, a squaring stage, and an add-one stage flow through a chain where no stage disturbs the order. Limiting measurement: when the consumer gives up early from the middle of the chain, the stages ahead of it never stop at all — even though every stage's closing responsibility is correctly set up, none of them knows when the consumer at the far end of the chain has given up. The fix is the subject of the next lesson.

The previous lesson measured how many sides the closing responsibility should belong to: zero
sides led to a leak, more than one led to a panic, and the correct answer was exactly one side.
But that production was a single step — input taken, squared, result written. Real processing
usually passes through more than one step, and each step has its own speed, its own output. This
lesson chains these steps: a **pipeline** — a chain where each stage takes the previous stage's
output as its own input, produces its own output, and **closes its own output channel itself**.
The closing responsibility is no longer in a single place; it sits separately in every link of
the chain.

Compared to the previous two lessons' arrangements, a pipeline's shape is different. In the
worker pool, many goroutines read from the **same** channel (a shared queue); in fan-out, many
goroutines wrote to the **same** channel (a shared destination). In a pipeline, each channel sits
between exactly one writer and one reader — no sharing, only chaining. This means the hardest
problem from the previous two lessons (how many sides should take on the closing responsibility)
never comes up here at all: every channel has exactly one owner, the stage that opens it. The
chain's own internal structure gets determinism for free; the real question in this lesson comes
up in its second half — from **outside** the chain, from a consumer giving up.

#### Pipeline: Every Stage Carries Its Own Closing Responsibility

The measurement below builds a three-stage line: `generate` produces a range of numbers,
`square` squares every value, `addOne` adds one to every value. All three are written the same
way: each opens a channel, starts a goroutine doing its own work, and once that goroutine
finishes its work, **closes its own channel itself** and returns the channel itself to its
caller.

```go
// stages.go — a three-stage pipeline: each stage opens its own output and closes it itself
package main

import "fmt"

// generate writes the values in 1..n to its own output and closes it once done.
func generate(n int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for i := 1; i <= n; i++ {
			out <- i
		}
	}()
	return out
}

// square replaces each value with its square; closes its own output once the input closes.
func square(input <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for value := range input {
			out <- value * value
		}
	}()
	return out
}

// addOne adds one to each value; closes its own output once the input closes.
func addOne(input <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for value := range input {
			out <- value + 1
		}
	}()
	return out
}

func main() {
	stage1 := generate(5)
	stage2 := square(stage1)
	stage3 := addOne(stage2)

	fmt.Println("-- three-stage pipeline, full consumption --")
	results := make([]int, 0, 5)
	for s := range stage3 {
		results = append(results, s)
	}
	fmt.Printf("value count flowing through: %d\n", len(results))
	fmt.Println("values:", results)
	fmt.Println("each stage's reason for stopping: what it waited for arrived (the previous stage's channel closed)")
}
```

```
-- three-stage pipeline, full consumption --
value count flowing through: 5
values: [2 5 10 17 26]
each stage's reason for stopping: what it waited for arrived (the previous stage's channel closed)
```

**CP11.** Five values (`1..5`) are squared in order (`1,4,9,16,25`) and each gets one added
(`2,5,10,17,26`) — the result list is printed in this exact order, and this order arises **with
no sorting anywhere at all**. The reason is different from the previous two lessons' fan-out/fan-in
arrangements: there, more than one producer wrote to the **same** channel at the same time, which
is why order could be lost and the collected results needed sorting. Here every channel has
**exactly one** writer and **exactly one** reader (`generate`'s single goroutine, `square`'s
single goroutine, the main goroutine itself); in a single-writer/single-reader relationship, a
channel always preserves send order, because there is no second side to race against.

The main goroutine itself is also part of the chain: it is the sole reader of `stage3`, and the
consuming loop runs until `addOne`'s goroutine closes its own output. Four goroutines run from
the start of the chain to the end — the three of `generate`, `square`, `addOne`, plus the main
goroutine itself — but none of them needs a special synchronization tool (a wait group, a
`select`) to wait for another; the channels themselves both carry the data and enforce the order.

**CP12.** Every stage's reason for stopping is the same too: `square` and `addOne` close their
own output channel once their own input channel closes (once `for value := range input` ends) —
`defer close(out)` guarantees this even if a panic happens while consuming the input. `generate`'s
own goroutine closes the same way with the same defer, once its loop ends after sending five
values. Every link in the chain applies the previous lesson's "the closing responsibility belongs
to exactly one side" principle **separately, for its own channel** — all three channels in the
chain are closed by their own producer, none of them is shared.

This arrangement's scaling rests on the same principle: if a fourth or fifth stage were added to
the chain, each new stage would only take the previous stage's output as its input and open its
own channel — a call like `stage4 := anotherTransform(stage3)` can be added to the chain without
touching any of the previous three stages at all. Determinism is independent of the chain's
length, because every added link follows the same single-writer/single-reader constraint; what is
true for a three-stage chain (order preserved, closing propagating on its own) stays true for a
ten-stage chain too.

The term `pipeline` is used here with a qualifier: this is a **stream-processing pipeline** — a
separate concept from a shell programming command pipeline or the steps in a continuous delivery
pipeline, sharing only the name. What is common between them is that in both, one output becomes
the next step's input; what differs is that a "stage" here is not a shell process or a deployment
step, it is a goroutine built around a pair of channels.

Every stage function's return type reflects this too: `generate`, `square`, and `addOne` all
return `<-chan int` — a channel that can only be **received from**, not sent to. This prevents
the caller from mistakenly sending its own value into the returned channel, at compile time; the
channel's **sole writer** is the goroutine of the function that created it, and the return type
encodes this exclusivity into the type system. The same restriction holds on the input parameters
too: `square` and `addOne` take `girdi <-chan int` — they can only read from their own channel,
they cannot write to it. This small type detail establishes the "every channel has exactly one
owner" principle not just as a design intention, but as a rule the compiler checks.

`square` and `addOne`'s bodies follow the exact same pattern, word for word: open an output
channel, start a goroutine, range over the input channel, transform each value and write it to
the output, close your own output once the input ends. This repetition is no accident — any
transforming stage follows the same three steps, the only thing that changes is the
transformation in between (`value * value` or `value + 1`). `generate` is one end of this pattern
(no input channel, only production) and the consuming loop in the main goroutine is the other end
(no output channel, only consumption); every stage in between repeats the same template, having
both an input and an output. No matter how many stages a chain has, the closing responsibility
always sits inside this same template, in the `defer close(out)` line.

#### Giving Up Early: The Stages Ahead Never Stop

The measurement above consumed **all the way to the end** of the chain — the loop ran until every
value in `stage3` was taken. But a consumer does not always go all the way to the end of the
chain: it might stop the moment it finds the first result it was looking for, it might get an
error, or it might give up for some other reason — a search operation, say, might not want the
remaining values at all once it finds its first match. The measurement below sets this situation
up in its plainest form: a two-stage line opens and the consumer gives up after the second value,
exiting the `for range` loop with a `break`.

```go
// early.go — if the consumer gives up early, the stages ahead of it never stop (fixed in the next lesson)
package main

import (
	"fmt"
	"runtime"
)

func generate(n int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for i := 1; i <= n; i++ {
			out <- i
		}
	}()
	return out
}

func square(input <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for value := range input {
			out <- value * value
		}
	}()
	return out
}

func main() {
	base := runtime.NumGoroutine()

	stage1 := generate(5)
	stage2 := square(stage1)

	taken := 0
	for range stage2 {
		taken++
		if taken == 2 {
			break // the consumer gives up after two values
		}
	}

	fmt.Println("-- consumer gives up after 2 values --")
	fmt.Printf("values taken: %d\n", taken)
	fmt.Printf("did a non-stopping stage remain: %v\n", runtime.NumGoroutine() > base)
}
```

```
-- consumer gives up after 2 values --
values taken: 2
did a non-stopping stage remain: true
```

**CP13.** After two values are taken, the loop exits with `break` — and the rest of the chain is
never told about this at all. Exactly where the chain gets stuck depends on timing, but it is
instructive to count the possible ends: either `square`'s goroutine gets stuck trying to send its
own squaring result to `stage2` — because the main goroutine no longer takes it — or it is
waiting to take the next value from `stage1`. If `square` is stuck sending, `generate`'s
goroutine also gets stuck when it tries to send its next value to `stage1`, because `square` is
now too busy to read it. Since the chain is short, both possible situations arrive at the same
result: **two** goroutines (`generate` and `square`) get permanently blocked. Since the consumer
took exactly two values, `square` gets stuck on its third send, `generate` on its fourth; which
order they get stuck in depends on timing, but the count of those stuck does not change — which
is why the measurement only prints the `runtime.NumGoroutine()` comparison, not how many
goroutines remain.

**CP14.** This arises for a **different reason** than the unclosed-channel leaks seen in the
previous two lessons. There, the problem was that closing was never done at all; here, every
stage correctly carries its **own** closing responsibility — both `generate` and `square` will
close their own output once their work is done. The problem is that the work never **finishes**:
`generate`'s "finishing" depends on it sending its fifth value, but if no one is taking after the
third value, the send never completes, and the `defer close` line is therefore never reached
either. A correctly built closing responsibility rests on the assumption that **every** link in
the chain waits for the other; this assumption breaks when the consumer exits early, and no stage
has any mechanism at all to notice this.

This is the chained version of the previous lesson's "even the correct setup assumes producers
will eventually finish" observation. There, there was a single assumption (the producers will
finish); here, **every** stage in the chain assumes the one before it and the one after it are
running normally, and the chain effect of these assumptions grows as the chain gets longer. In a
three-stage chain, a consumer exiting early can affect two goroutines; in a ten-stage chain, the
same early exit can block nine goroutines — the size of the leak grows together with the chain's
length, independent of how early the consumer gave up.

Placing the three lessons' leak measurements side by side makes the difference clear. In the
worker pool, the cause of the leak was closing being **forgotten** — the `close(jobs)` line was
never written at all. In fan-out, the cause was the responsibility being **given to no one at
all** — the third-party goroutine was never set up. Here, the responsibility is both written and
given to the right side, but the **event** that should trigger it (the input running out) never
happens at all, because the side that would have consumed the input has already stepped away.
All three are caught by the same boolean measure (`runtime.NumGoroutine() > base`), but the root
cause of all three sits in three separate places: missing closing, ownerless closing, untriggered
closing.

This limit is **not solved** within the lesson — it is only measured. Equipping a stage with the
information "the consumer is no longer listening" requires that stage to check a signal coming
from **outside** its own loop; every stage's `for value := range input` loop would need an
additional branch, and that branch would need to come from a single source reaching **all**
stages at the same time. This is exactly the next lesson's subject.

#### Summary

- In a pipeline (stream-processing pipeline), each stage opens its own output channel and closes
  it itself; the closing responsibility does not sit in a single place, it sits separately in
  every link of the chain.
- Because every channel in the chain has exactly one writer and one reader, order is preserved
  with no sorting tool needed at all — unlike the shared channels in fan-out/fan-in.
- The term `pipeline` is used here in the stream-processing sense, a separate concept from a
  shell or delivery pipeline.
- When the consumer exits the chain early, the stages ahead of it never find out and stay
  permanently blocked; the leak is again measured only as a boolean.
- This leak's source is different from the previous lessons' unclosed channels: the closing
  responsibility is correctly built, but the work itself never finishes.
- The size of the leak grows together with the chain's length: a consumer exiting early can
  block one goroutine in a short pipeline, the rest of the chain in a long one.

#### Next Step

The fix for the leak seen in this lesson requires a stage to listen not only to its own input
channel, but also to a **give-up signal coming from outside** — and this signal needs to reach
every stage from the beginning to the end of the chain **at the same time**, without being passed
to each stage by hand one by one. The next lesson measures the tool that carries this signal: how
a cancellation reaches every waiter in the chain at the same time, and what happens when a
goroutine never checks this signal at all.

### The Context Package

Source: https://academia.sh/en/courses/go-concurrency/context-package

A single cancellation reaches all four goroutines waiting on ctx.Done() at the same time; all of them stop in the 'cancelled' class and ctx.Err() carries the same fixed reason. What a request-scoped value (context.WithValue) is for, and what it is not for, are measured separately. Limiting measurement: a context does not force anyone to stop, it only notifies — a goroutine that never checks ctx.Done() completes its own work to the end, even though the context has already been cancelled.

The previous lesson measured a leak and left it unsolved: when a consumer exited a pipeline
early, the stages ahead of it had no way at all to know about it. It was not enough for the
stages to listen to their own input channels — a signal was needed that came from outside and
reached **all** stages at the same time. This lesson measures the tool that carries that signal:
the context package (the `context` package). A context being cancelled reaches every goroutine
waiting on it, however many there are, through a single event — unlike a channel, there is no
"how many receivers" question here at all, because a context's cancellation signal works like a
**broadcast**, not to a single receiver but to all of them at once.

The `context.Context` type the context package introduces is not, on its own, a new mechanism —
underneath it is only a channel (the one `Done()` returns) and an error value (the one `Err()`
returns). What is new is that this pair can be derived in a **tree** shape: when a new context is
derived from a context (with calls like `context.WithCancel`, `context.WithTimeout`), cancelling
the parent context automatically cancels every child context too. This lesson starts with a
single-layer tree (a single context derived from `context.Background()`); the tree itself is not
this lesson's subject, but how many waiters a single node's cancellation reaches is measured here
in clear terms.

#### One Cancellation, Every Waiter in the Chain

The measurement below starts four goroutines; each one waits on `ctx.Done()`. After a single
`cancel()` call, all four send their report — all in the same class: **cancelled**.

```go
// cancelspreads.go — a single cancellation reaches every goroutine waiting on the context at the same time
package main

import (
	"context"
	"fmt"
	"sort"
	"sync"
)

type report struct {
	id     int
	reason string
}

func waiter(id int, ctx context.Context, reports chan<- report, group *sync.WaitGroup) {
	defer group.Done()
	<-ctx.Done()
	reports <- report{id: id, reason: "cancelled"}
}

func main() {
	const waiterCount = 4
	ctx, cancel := context.WithCancel(context.Background())

	reports := make(chan report, waiterCount)
	var group sync.WaitGroup
	for id := 1; id <= waiterCount; id++ {
		group.Add(1)
		go waiter(id, ctx, reports, &group)
	}

	cancel()
	group.Wait()
	close(reports)

	records := make([]report, 0, waiterCount)
	for r := range reports {
		records = append(records, r)
	}
	sort.Slice(records, func(i, j int) bool { return records[i].id < records[j].id })

	fmt.Println("-- one cancellation, four waiters --")
	for _, r := range records {
		fmt.Printf("waiter %d  reason=%s\n", r.id, r.reason)
	}
	fmt.Println("ctx.Err():", ctx.Err())
}
```

```
-- one cancellation, four waiters --
waiter 1  reason=cancelled
waiter 2  reason=cancelled
waiter 3  reason=cancelled
waiter 4  reason=cancelled
ctx.Err(): context canceled
```

**CP15.** What `context.WithCancel` returns as `ctx.Done()` is a **channel**, but not an
ordinary one: when `cancel()` is called, this channel **closes**, and the "a receive from a
closed channel does not block" behavior seen repeatedly in the previous lessons applies here too.
All four goroutines were waiting on the `<-ctx.Done()` line; the moment the channel closes, all
four are freed at the same time. Closing belonged to a **single** side in every previous
lesson too — a producer, a stage, or a third party (like the one in the Fan-Out and Fan-In lesson
who knew when all producers had finished); it is the same here too. `cancel` itself wraps a
single close call; even more than one `cancel` call closes the channel only once. This is the
context package's counterpart to the Fan-Out and Fan-In lesson's "closing a channel is an atomic
operation" observation. This, too, is a deliberate design decision: it is safe to call `cancel`
more than once, even from more than one goroutine — the caller does not need to track its own
state wondering "did I already cancel this," because the second and later calls silently do
nothing at all.

Sorting the four reports by `id` (`sort.Slice`) comes from the same need seen in the previous
lessons' fan-in channels: four goroutines can write to the `reports` channel at the same time,
and which one arrives first depends on timing. The difference is that this time sorting does not
affect the **result** — all four are already in the same class (cancelled), sorting only makes
the output readable and repeatable.

**CP16.** All four stopping in the same class (**cancelled**) is the plainest form, in this
context, of the four-class measure established in the course's first lesson (on its own, what it
waited for arrived, cancelled, never stopped): none of them finished its own body, none was
waiting for another goroutine's step, all four stopped as the direct result of a single
cancellation event. The text `ctx.Err()` returns is fixed too — "context canceled" — a
version-independent, fixed string defined by the language's own standard library; a caller
wanting to tell which context was cancelled for which reason can read this text.

These four were, in the previous lessons, the "cancelled" class's **only** example (two waiters
in the shared definition) — here that same class is raised fourfold and the result does not
change, because a channel closing shows the same behavior no matter how many receivers there are.
This scale-independence is the context package's real strength: on a web server, the
cancellation of a single request can reach dozens of goroutines handling that request (a database
query, a few outside calls, a cache read) through the same `ctx.Done()` channel, without needing
to warn any of them one by one.

#### Carrying a Request-Scoped Value

The context package's second job is not spreading cancellation, it is carrying a **request-scoped
value** through the call chain — for example, carrying a request ID down to the deepest call,
without changing the signature of every function in the chain. This is a separate need that none
of the previous lessons' channel arrangements meets: channels **stream** data, while context
values carry it **alongside** the call chain. In all three of the worker pool, fan-out/fan-in, and
pipeline measurements, data always flowed through an explicit parameter or an explicit channel;
what `context.WithValue` solves is different — carrying a request ID down to the deepest point in
the chain, without adding it separately to the signature of every function handling that request.

Carrying a value works independently of spreading cancellation: the measurement below first sets
up a value-carrying arrangement, then (in the same program, with a separate context) sets up the
limiting measurement showing that cancellation does not stop anyone.

```go
// valueignore.go — carrying a request-scoped value, and a goroutine that never checks cancellation
package main

import (
	"context"
	"fmt"
)

type keyType string

const requestKey keyType = "request-id"

func readDeep(ctx context.Context) string {
	value, ok := ctx.Value(requestKey).(string)
	if !ok {
		return "none"
	}
	return value
}

func main() {
	ctx := context.WithValue(context.Background(), requestKey, "request-42")
	fmt.Println("request id read by the inner function:", readDeep(ctx))

	ctx2, cancel := context.WithCancel(context.Background())
	cancel() // this goroutine has not started yet, but cancellation already happened

	steps := make(chan int, 3)
	go func() {
		// ctx2.Done() is never checked; this goroutine has no idea it was cancelled.
		for step := 1; step <= 3; step++ {
			steps <- step
		}
		close(steps)
	}()

	completedSteps := 0
	for range steps {
		completedSteps++
	}
	fmt.Println("was the context already cancelled while the goroutine ran:", ctx2.Err() != nil)
	fmt.Printf("completed step count: %d / 3\n", completedSteps)
}
```

```
request id read by the inner function: request-42
was the context already cancelled while the goroutine ran: true
completed step count: 3 / 3
```

**CP17.** `requestKey` is not an ordinary string, it is a constant of its own defined
`keyType` type — this prevents another package, at compile time, from using the same string
value as its own key and accidentally writing into the same context slot. `readDeep` reads what
`ctx.Value` returns with a **type assertion** (`.(string)`) and separates out, with the second
return value (`ok`), the possibility that the value was never found at all — this is the context
value's counterpart to the two-value-return access pattern in the dictionary (from **M09/K01**).

What this way of carrying a value **is not** is also part of the measurement: `context.WithValue`
exists not to carry a **required** dependency or parameter a function needs, but for
request-specific, header-like data (a request ID, a trace token) that has to pass through
**every** layer of a call chain, most of which do not use it. The `readDeep` function does not
crash if it cannot find the request ID (`ok` is `false`) — it just returns "none," because a
context value is treated as **optional**. A value that is mandatory for a function to work (a
database connection, a piece of configuration) should be passed directly as a parameter, not
through the context; otherwise the function's signature gives up saying what it needs at all.

This distinction's practical consequence cannot be seen by looking at a function's signature like
`readDeep`'s: the signature `func readDeep(ctx context.Context) string` never says the function
needs a request ID at all — it only takes a context, and what is inside that context stays
unknown until call time. This is an openness similar to the `nil` map and `nil` pointer behaviors
in the Go Fundamentals course: the type system does not check **which** keys a context carries, it
only reports the absence with `nil`/`ok=false` when a key is asked for. The only way to know
which context keys a function reads is to read the source — the signature makes no promise on
this at all.

#### A Context Does Not Stop Anyone, It Only Notifies

The second measurement proves this directly. `ctx2` was already cancelled **before** the goroutine
even started — the `cancel()` call runs before the `go func() {...}()` line. This ordering is
deliberate: the measurement's purpose is to eliminate entirely a timing debate like "the
goroutine started while cancellation had not happened yet, then cancellation could not catch up."
The context is already cancelled before the goroutine even exists; despite this, the goroutine
that gets started never checks `ctx2.Done()` at all, it just produces its three steps in order
and closes its channel.

**CP18.** The result is clear: `ctx2.Err() != nil` is `true` — the context was **already**
cancelled by the time the goroutine started running — yet the `completed step count` is still
`3 / 3`. Since the goroutine never looks at whether the context was cancelled at all, it kept
running **completely unaware** of the cancellation and finished its work in full. This is the
exact answer to the question left open at the end of the previous lesson: the context package
does not force anyone to stop, it is not a mechanism that **cuts off** a goroutine. All it does is
notify everyone who asks, once cancellation happens — those listening to `ctx.Done()` take this
notice and make their own decision, those not listening notice nothing at all.

This behavior might look like a defect at first glance — why does the context not stop a goroutine
by itself? The answer lies in Go's overall design: the language's runtime offers no tool at all
for **forcibly** stopping a goroutine from the outside (there is no call in Go resembling cutting
off a thread from outside). A goroutine can only stop by its own choice, on its own code path, by
reaching a `return` or the end of the function. The context package works within this
constraint: since it offers no forced stopping, the only tool it has is **notification** —
telling a goroutine that thinks it should stop, and leaving the decision to it. This is the
clearest example of the principle seen repeatedly in the previous lessons — "the decision to stop
always depends on a check point in the goroutine's own body": the check point here is a
`case <-ctx.Done()` branch inside a `select`, and if that branch was never written in the source,
the cancellation itself carries no meaning at all.

This also draws the boundary of the fix for the pipeline leak measured in the previous lesson:
adding a `select` branch that checks `ctx.Done()` to a stage makes that stage **aware of
cancellation** — but this addition has to be made separately in every stage. If the `square`
stage, say, had written a two-branch `select` instead of a plain send to its output — one
sending the value, the other waiting on `ctx.Done()` — cancelling the context could have freed
this stage too once the consumer exited early. But this change has to be made separately in
**every** stage of the chain; the context itself never automatically stops any goroutine, what
spreads is only **information**, and turning that information into action stays each goroutine's
own responsibility.

This nature of a cancellation context explains why a `context.Context` value does not work like a
lock or an interrupt: it is only a closing channel, and whoever is listening to that closing
channel is the only one who reacts. This also sets the context package on the same footing as
every mechanism in the previous lessons — a channel, a wait group, a `select`, all rest on the
same principle: concurrency in Go is not built by managing a goroutine from the outside, it is
built by a network of channels through which goroutines **notify** each other.

#### Summary

- A context being cancelled reaches every goroutine waiting on `ctx.Done()`, however many there
  are, at the same time; all of them stop in the same class (cancelled).
- `ctx.Err()` returns a fixed text defined by the language's standard library; it can be read to
  tell which context was cancelled.
- `context.WithValue` is for optional, header-like data carried between layers; it does not
  stand in for a function's required dependencies.
- A context does not force anyone to stop, it only notifies — a goroutine that never checks
  `ctx.Done()` completes its work to the end even if the context has been cancelled.
- Making a goroutine responsive to cancellation requires it to add a `ctx.Done()` check to its
  own loop; this does not happen automatically.
- Go's runtime offers no tool for forcibly stopping a goroutine from the outside; the context
  works within this constraint, and the only tool it has is notification, not force.

#### Next Step

In this lesson, exactly **when** the context itself was cancelled was already known in advance —
`cancel()` was called explicitly, and the waiters were waiting for that call. But the most common
reason a context gets cancelled in real systems is not someone making a deliberate decision, it is
**a duration running out**: waiting for a network call or a database query to respond within a
reasonable time, and giving up if it does not. The next lesson measures this special case: when a
`select` statement picks a branch based on whether it received a result or a cancellation,
a timeout becomes a form of this same choice — what is measured is not the elapsed duration, but
which branch was chosen; this lesson measured an already-known cancellation, the next lesson
measures a cancellation that has **not yet happened** (a timeout) with the same `select`
mechanism.

### Timeouts and Deadlines

Source: https://academia.sh/en/courses/go-concurrency/timeouts-and-deadlines

A timeout is a branch choice in a select statement: if a result is already ready, the result branch is chosen; if the context is already cancelled (representing a timeout), the cancellation branch is chosen — both deterministic, because only one branch in the setup is actually ready. Duration is never measured anywhere, the only thing printed is which branch was chosen. Limiting measurement: a timeout rescues the waiting side but does not stop the awaited work — a work goroutine whose result no one will take anymore is left behind, as a leak, behind the caller who exited its branch.

The previous lesson measured how a context's cancellation reaches waiters **after** that
cancellation had already happened — `cancel()` was called explicitly, and the waiters were
waiting for that call. But in real systems, the most common cause of cancellation is not a
person's decision, it is **a duration running out**: waiting for a network call to respond within
a reasonable time, and giving up if it does not. This lesson measures a timeout — but not as a
duration. A timeout reduces, in fact, to one of two branches of a `select` statement being
chosen: did a result arrive, or did a cancellation arrive. This lesson builds, without ever
measuring duration, which branch gets chosen, in a **deterministic** way.

The context package offers two separate constructors for this: `context.WithTimeout` builds a
context that cancels after a **relative** duration from now; `context.WithDeadline` states an
**absolute** point on the calendar (a deadline) and cancels once that point is reached. Both
arrive at the same result — a `Done()` channel that closes on its own at a certain point — and as
far as `select` is concerned, neither is distinguishable in any way from the previous lesson's
`context.WithCancel`. This lesson therefore does not build the two separately; it models the
common point both arrive at — "the context has closed at some point" — and never prints a real
duration or a real calendar value at all.

#### A Branch Choice: A Result, or a Cancellation

The measurement below places two setups side by side. In the first, the result is **already
ready**: by the time `select` runs, a value is already waiting on the result channel, and the
context has not been cancelled. In the second, the context is **already cancelled** —
representing a timeout — and the result channel is empty, with no sender at all. In both setups,
which branch `select` will choose is known in advance, because in the setup itself only **one**
branch is really ready.

```go
// branchchoice.go — a timeout is a branch choice; what is measured is not the duration but which branch is chosen
package main

import (
	"context"
	"fmt"
)

func chooseBranch(ctx context.Context, result <-chan string) string {
	select {
	case value := <-result:
		return "result: " + value
	case <-ctx.Done():
		return "cancelled: " + ctx.Err().Error()
	}
}

func main() {
	fmt.Println("-- setup 1: result already ready --")
	ctxReady, cancelReady := context.WithCancel(context.Background())
	defer cancelReady()
	resultReady := make(chan string, 1)
	resultReady <- "completed"
	fmt.Println("chosen branch:", chooseBranch(ctxReady, resultReady))

	fmt.Println()
	fmt.Println("-- setup 2: context already cancelled (represents a timeout) --")
	ctxCancelled, cancel := context.WithCancel(context.Background())
	cancel()
	resultEmpty := make(chan string)
	fmt.Println("chosen branch:", chooseBranch(ctxCancelled, resultEmpty))
}
```

```
-- setup 1: result already ready --
chosen branch: result: completed

-- setup 2: context already cancelled (represents a timeout) --
chosen branch: cancelled: context canceled
```

**CP19.** In both setups, the moment `chooseBranch` is called, only **one** branch stands ready
in front of `select` — this is the guarantee that the result was not chosen at random. In setup
1, `resultReady` is a buffered channel and a value was placed into it **before** `select` runs;
`ctxReady` has not been cancelled, meaning `ctx.Done()` never closed at all. `select`, in a
situation where only one of two branches is ready, always chooses that branch — the choice here
depends not on chance but on the setup itself. Setup 2 is the mirror image: `ctxCancelled` was
cancelled before `chooseBranch` is called, while `resultEmpty` is completely empty with no
goroutine writing to it at all — the `ctx.Done()` branch is the only one ready here.

These two setups were deliberately placed at the extremes: in a real race, both branches can
become ready **at the same time** (if the result lands at exactly the moment of the timeout),
and in that case `select` decides which one to choose at random — this is the same randomness
rule established in the `select` Statement lesson. But this lesson never builds that common
point at all: both setups are situations where only one branch is ready, and the other definitely
is not. Determinism comes from exactly here — the measurement chooses the two corners where
**no** randomness exists, and never runs the middle ground where randomness could occur at all.

**CP20.** A real timeout is built with `context.WithTimeout`, and the context cancels **on its
own** after a certain duration — the setups this lesson measures **model** that real timing,
without ever waiting for a real duration to pass at all. `ctxCancelled` in setup 2 being "already
cancelled" produces the same result as "a timed-out context whose duration has run out": as far
as `select` is concerned, the visible face of both is the same, because `select` does not know
**why** a context was cancelled, it only sees whether the `Done()` channel is closed or not. This
is what makes the measurement as valid as a real timeout — the only difference is **when** the
cancellation was triggered, not in `select`'s own behavior.

Duration never appears in any line here: no `time.Sleep`, no `time.After`, no measured delay. The
only thing measured is **which branch was chosen** — this is the direct application of the
previous lesson's "not duration, the chosen branch is written" rule. Why establishing order with
`time.Sleep` is forbidden is visible here once again: if this measurement were built with a real
timeout duration, which branch would win would depend on the machine's load at that moment —
modeling removes this uncertainty entirely.

The `chooseBranch` function itself is **the same** in both setup 1 and setup 2 — no branching, no
special-case code at all. This is the most practical side of the timeout arrangement: making a
function responsive to a timeout does not require adding an "if the duration has run out"
check inside that function; it is enough just to add the cancellation branch into a `select`, and
the rest is already handled by the context, whose own closing responsibility is already built.
The same `chooseBranch` serves its caller equally well whether it receives a cancellation from
`WithCancel` or a timeout from `WithTimeout`, with no change needed at all.

#### A Timeout Rescues the Waiter, It Does Not Stop the Awaited Work

Once the `chooseBranch` call above returns, the calling side is **rescued** — it is no longer
waiting, it can move on with a result or a cancellation text. But this rescue does not mean that
the work expected to produce the result has stopped too. The measurement below builds on setup 2:
**after** the caller exits the cancellation branch, a work goroutine that is actually producing
the result starts and tries to send its result — but there is no one left to take it anymore.

```go
// leak.go — a timeout rescues the waiter but does not stop the awaited work
package main

import (
	"context"
	"fmt"
	"runtime"
)

func chooseBranch(ctx context.Context, result <-chan string) string {
	select {
	case value := <-result:
		return "result: " + value
	case <-ctx.Done():
		return "cancelled: " + ctx.Err().Error()
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // represents a timeout: the caller has already given up

	result := make(chan string) // unbuffered, no sender yet

	fmt.Println("-- caller exits its branch --")
	fmt.Println("chosen branch:", chooseBranch(ctx, result))

	// The caller left. To model the work as if it is still running, the work
	// goroutine now starts and tries to write its result to the same channel.
	// It never checks the context, so it keeps going, but there is no receiver left.
	base := runtime.NumGoroutine()
	go func() {
		result <- "late result"
	}()

	fmt.Printf("is the work goroutine still running (leak): %v\n", runtime.NumGoroutine() > base)
}
```

```
-- caller exits its branch --
chosen branch: cancelled: context canceled
is the work goroutine still running (leak): true
```

**CP21.** When `chooseBranch` is called, no goroutine has yet written to the `result` channel —
this is why the chosen branch is definitively `ctx.Done()`. The work goroutine only starts
**after** `chooseBranch` returns; this ordering is deliberate, because the goal is to remove
entirely the question of whether the work goroutine existed **during** the `select` from being a
matter of timing. Once the work goroutine starts, it tries to send its own result to the `result`
channel, but the channel is unbuffered and there is no one left reading it at all —
`chooseBranch` has already returned, the main goroutine is no longer listening to `result`. The
send is permanently blocked.

**CP22.** The result `runtime.NumGoroutine() > base` comes out `true` — this is the boolean leak
measure seen repeatedly in the previous lessons, appearing here for the third time, but for a new
reason: in the worker pool the cause was closing being forgotten, in fan-out it was
responsibility never being given to anyone, in the pipeline it was the work never finishing.
Here the cause is entirely different — there is not even a channel to close, the problem is that
the work goroutine **has no idea at all who is listening**. `chooseBranch` choosing the
`ctx.Done()` branch tells the work goroutine nothing at all; the work goroutine keeps doing its
own share of the computation and keeps trying to send its result, exactly like the previous
lesson's negligent goroutine finishing its work without ever checking cancellation.

That `base` is captured a second time in this measurement, right **before** the work goroutine
starts, is also worth noting: the first two setups' own goroutines (if any) may have already
finished, and the measurement needs to focus only on the single goroutine in the third setup.
This is the same discipline as the `runtime.NumGoroutine()` usages in the previous lessons: the
baseline is always taken right before the goroutine to be measured starts, so the comparison
reflects only whether that one goroutine exists or not — the count of other goroutines that have
been running since the program started never gets mixed in.

This explains why the lesson's two title words ("timeout" and "resource leak") are mentioned
together. A timeout **rescues** the caller from a waiting state — the caller is no longer blocked
forever, it can move on with a response (a cancellation or a result). But what a timeout rescues
is only the **waiting side**; the work that was expected to produce the result is entirely
unaware of the timeout, and keeps running at its own pace. When the caller says "I've given up,"
this news never reaches the work goroutine at all — just like the previous lesson, the context
does not force anyone to stop, it only opens an exit door for the waiting side.

The work goroutine in this measurement was deliberately built in its plainest form: a single
send, no computation at all. In a real system, this goroutine would be a network request, a
database query, or a file read — work with its own duration, wanting to complete without being
interrupted. A timeout does not stop that work itself; it only lets the **caller** give up
waiting for that work. Whether the work actually completes in the background is not this
measurement's subject — what is certain here is only that the work's result will **no longer be
taken by anyone**, because the unbuffered channel's sole reader has already left.

The practical consequence of this is clear: setting up a timeout alone is not enough. The work
goroutine itself must also **share** the same context and check its own `ctx.Done()` — otherwise,
while the caller says "I timed out" and moves on, the work left behind can keep running forever.
This lesson does not build this second step, it only measures what happens when it is not built;
having the work goroutine take the same `ctx` and check it in its own `select` would be a direct
repeat of the pattern seen in the previous two lessons (adding a `ctx.Done()` branch to a stage).

This topic's five lessons measured five separate faces of the same problem, and all of them meet
at the same point: unless a goroutine checks its own `ctx.Done()` or its own closing channel,
no matter how well-intentioned a mechanism is set up, no signal coming from outside can stop it.
In the worker pool, this check was the `for job := range jobs` loop itself; in the pipeline, it
was every stage's own input channel; here, it is the missing `select` branch the work goroutine
never wrote. All five repeat the same lesson from a different angle: stopping depends on a
goroutine reaching a check point in its own code path; with no check point, no outside signal
carries any meaning at all.

#### Summary

- A timeout is a branch choice in a `select` statement; duration is never measured, only which
  branch was chosen is written down.
- If a result is already ready, or if the context is already cancelled, the branch `select`
  chooses is deterministic — because only one branch in the setup is really ready.
- A cancelled context's appearance, as far as `select` is concerned, is the same as a timed-out
  context whose duration has run out; `select` does not know the reason for cancellation, it only
  sees whether `Done()` is closed or not.
- A timeout rescues the waiting side but does not stop the work expected to produce the result;
  the work goroutine keeps running as long as it does not check its own context.
- This last leak, too, is measured as a boolean; its cause differs from the previous lessons'
  three causes (forgotten closing, ownerless responsibility, unfinished work) — a sender with no
  idea who is listening.
- `context.WithTimeout` (a relative duration) and `context.WithDeadline` (an absolute point) are
  indistinguishable from `context.WithCancel` as far as `select` is concerned; all three give a
  `Done()` channel that closes on its own.

#### Next Step

Every mechanism built across this topic's five lessons — the worker pool, fan-out and fan-in, the
pipeline, the context, the timeout — rested on the same principle: goroutines notified each other
through **communication**, by sending on a channel or by closing one. A timeout rescues the
waiter but does not stop the awaited work; the next topic turns to shared state — what changes
once, instead of communication, the choice is to protect a common value. There, goroutines no
longer notify each other over a channel; they limit their access to the same memory with a lock —
and the decision to stop still depends on a step taken by whoever is holding that lock.

## Shared State

Mutual exclusion measured through lock scope, the counter contract of collective waiting, where once and atomic operations sit relative to the lock, and what the race condition detector does and does not find during testing.

### Mutexes

Source: https://academia.sh/en/courses/go-concurrency/mutexes

A goroutine blocks until the other goroutine holding the lock releases it, and its termination class is waited-for-arrived. The same counter is run through two setups, locked and unlocked, with 20 goroutines and 100 increments per goroutine, and both give 2000 — because the unlocked setup never sets up a real race, since each goroutine writes to its own cell. The limiting measurement models a single write that falls outside the lock's scope with an order forced by a channel: two increments are applied, but the result comes out as 1 instead of 101.

The previous lesson's closing drew a boundary: a timeout rescues the waiting goroutine but
does not stop the waited-for work itself — the worker goroutine keeps running, only nobody
is waiting for it anymore. This lesson moves away from that question. Every lesson so far
measured goroutines passing a value to one another: a number crossing a channel, a
context's cancellation signal, the branch a timeout selects. In this lesson, goroutines do
not send anything to each other; they share the same value. What changes when the choice is
to protect shared state instead of communicating?

Channel-based mechanisms always dodged this question: a value crossing a channel was
copied, the sending side never touched it again after sending, so a situation where two
goroutines touch the same memory at the same time was never even set up. Shared state
closes off that escape: more than one goroutine genuinely reads and writes the same
variable. The course's second hard constraint kicks in here — this lesson does not run an
unprotected version of the sharing to produce a wrong result; instead, it measures what the
protection provides and where it stops.

#### The Lock's Scope

The concept of mutual exclusion — forcing only one execution unit to access a resource at a
time — was established in the Operating System Concepts course and measured there by
counting interleavings; it is not repeated here. What this lesson measures is not the
concept itself but Go's one tool for realizing it: `sync.Mutex`, a mutex. The contract the
lock offers is short: a `Lock` call returns if someone else is holding the lock, but blocks
first; `Unlock` releases the lock. The lines between `Lock` and `Unlock` are the lock's
**scope** — the lock guarantees only that these lines run under exactly one goroutine at a
time, and it has nothing to say about anything outside that scope. This scope is Go's
counterpart to what the Operating System Concepts course calls the critical section: the
concept of a critical section was established there independent of how it gets bounded,
and `sync.Mutex` here makes it concrete as an interval between two calls.

It is also part of the contract that `sync.Mutex` needs no setup beyond a declaration: the
moment `var lock sync.Mutex` is written, the lock is ready to use, because a lock's zero
value is already an unlocked lock. A single-sentence reference to the Go Fundamentals
course's discussion of zero values is enough here — the lock itself is not this course's
measurement axis, only a tool.

The `defer s.mu.Unlock()` line in the `Increment` method is also part of this contract: the
call that releases the lock runs no matter where the method body returns from — a normal
return, an early return, or a panic. Releasing the lock by hand, with an `Unlock()` call
written at the end of the body, leaves the door open for the lock to stay held forever once
an early return is added in the middle of the body; `defer` closes that gap. This lesson
does not run that gap separately, because the topic is already addressed in this lesson's
limiting measurement, in a different form — as the lock's own scope being drawn
incorrectly.

The first consequence of this contract ties directly back to the question asked since the
course's first lesson: how will a goroutine waiting on a lock stop? The answer is one of
the shared definition's own lines — when the goroutine holding the lock calls `Unlock`, the
waiting goroutine's `Lock` call returns and the goroutine continues. This is an instance of
waited-for-arrived, one of the four termination classes: the waiting goroutine does not
stop on its own; a step taken by another goroutine — releasing the lock, here — sets it
free. When more than one goroutine is waiting on the same lock, `sync.Mutex` gives no
guarantee about which one proceeds first; this belongs to the same family as the Operating
System Concepts course's discussion of starvation, and it is mentioned here in only one
sentence, because it does not matter for this lesson's single waiter.

```go
// counter.go — a counter protected by a mutex, and a half-protected counter modeling the lock's boundary
package main

import "sync"

// ProtectedCounter carries an integer protected by a mutex.
type ProtectedCounter struct {
	mu    sync.Mutex
	value int
}

func (s *ProtectedCounter) Increment() {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.value++
}

// HalfProtected splits the increment into two: the read inside the lock, the write outside it.
// This models the single line that falls outside the lock's scope.
type HalfProtected struct {
	mu    sync.Mutex
	value int
}

func (y *HalfProtected) ReadAndUnlock() int {
	y.mu.Lock()
	defer y.mu.Unlock()
	return y.value
}

func (y *HalfProtected) WriteUnlocked(newValue int) {
	y.value = newValue
}
```

`ProtectedCounter` sets up the contract correctly: everything `Increment` reads and writes
stays between `Lock` and `Unlock`. `HalfProtected`, by contrast, is built broken on
purpose — the read under the lock, the write entirely outside it, in two separate methods.
This second type is for the limiting measurement below; the first measurement uses only
`ProtectedCounter`.

#### Three Measurements, One Run

```go
// main.go — lock scope, a locked/unlocked comparison, modeling the lock's boundary
package main

import (
	"fmt"
	"sync"
)

func main() {
	fmt.Println("-- a goroutine waiting on the lock stops once the lock is released --")
	var lock sync.Mutex
	report := make(chan string, 1)
	lock.Lock()
	go func() {
		lock.Lock()
		lock.Unlock()
		report <- "stopped: waited-for-arrived"
	}()
	lock.Unlock()
	fmt.Println(<-report)

	fmt.Println()
	fmt.Println("-- result in the locked and unlocked setups --")
	const goroutineCount = 20
	const incrementCount = 100

	locked := &ProtectedCounter{}
	var group1 sync.WaitGroup
	group1.Add(goroutineCount)
	for i := 0; i < goroutineCount; i++ {
		go func() {
			defer group1.Done()
			for j := 0; j < incrementCount; j++ {
				locked.Increment()
			}
		}()
	}
	group1.Wait()

	shares := make([]int, goroutineCount)
	var group2 sync.WaitGroup
	group2.Add(goroutineCount)
	for i := 0; i < goroutineCount; i++ {
		go func(index int) {
			defer group2.Done()
			for j := 0; j < incrementCount; j++ {
				shares[index]++
			}
		}(i)
	}
	group2.Wait()
	unlockedTotal := 0
	for _, s := range shares {
		unlockedTotal += s
	}

	fmt.Printf("locked total   : %d\n", locked.value)
	fmt.Printf("unlocked total : %d\n", unlockedTotal)
	fmt.Printf("are they equal : %v\n", locked.value == unlockedTotal)

	fmt.Println()
	fmt.Println("-- limiting measurement: modeling a single write that falls outside the lock's scope --")
	half := &HalfProtected{}
	step1 := make(chan int)
	step2 := make(chan struct{})
	done := make(chan struct{})

	go func() {
		old := half.ReadAndUnlock()
		step1 <- old
		<-step2
		half.WriteUnlocked(old + 1)
		close(done)
	}()

	oldValue := <-step1
	half.mu.Lock()
	interimValue := half.value
	half.value = interimValue + 100
	half.mu.Unlock()
	close(step2)
	<-done

	fmt.Printf("old value g1 read          : %d\n", oldValue)
	fmt.Printf("two increments applied (+1 and +100), expected total: 101, actual: %d\n", half.value)
}
```

```
-- a goroutine waiting on the lock stops once the lock is released --
stopped: waited-for-arrived

-- result in the locked and unlocked setups --
locked total   : 2000
unlocked total : 2000
are they equal : true

-- limiting measurement: modeling a single write that falls outside the lock's scope --
old value g1 read          : 0
two increments applied (+1 and +100), expected total: 101, actual: 1
```

**SS1.** The first line runs the shared definition's own measurement directly: `main`
starts a goroutine while holding the lock, that goroutine waits on its `Lock` call, and
once `main` calls `Unlock`, the goroutine continues and reports its reason for stopping.
This mechanism is exactly one of the "waited-for-arrived" lines in the 2/5/2/3 distribution
measured in the Goroutines lesson — the lock is the fifth mechanism here.

**SS2.** The second measurement runs twenty goroutines, each incrementing a hundred times,
through two separate paths. In the `locked` setup, all of them share the same
`ProtectedCounter`, and every increment stays between a `Lock`/`Unlock` pair; the result is
deterministically 2000. In the `unlocked` setup, each goroutine instead increments a
separate slice cell belonging to its own index — twenty goroutines write to twenty
different memory locations, and none of them touches the same one. Both setups give 2000,
and the two numbers are equal because neither one actually set up a race that needed a
lock: in the locked setup the lock provides extra protection but is not protecting a
resource that was ever shared to begin with, and the unlocked setup excludes the race from
the start by using separate memory instead of sharing. The results coming out equal does
not mean "unlocked is safe too"; it only means both setups are safe, for different reasons.
We do not run the result produced by goroutines writing without a lock to a genuinely
shared single variable in this lesson — that comparison falls under this course's second
hard constraint and is measured only through the answer the detector gives.

The lock itself has a cost: twenty goroutines wait in turn for the same lock, meaning each
`Increment` call takes over the lock the previous one released. This **contention** — more
than one goroutine trying to reach the same lock at the same time — can make the locked
setup slower than the unlocked one, because in the unlocked setup no goroutine waits for
another. But this lesson does not measure duration; the only thing measured is correctness,
both setups reaching the same total. The cost of contention is real, but it falls outside
this lesson's question — a lock exists for correctness, not for speed.

#### Limiting Measurement: The Boundary of the Lock's Scope

**SS3.** `HalfProtected` carries the same lock as `ProtectedCounter` but breaks the
contract: it reads the value and releases the lock immediately, then writes the new value
outside the lock. The measurement does not leave this gap to random timing — two channels
(`step1`, `step2`) step in and force the order exactly: the goroutine first reads the old
value and reports it, `main` takes its own lock only after receiving that report,
increments the value by 100, and releases the lock, and only then is the goroutine
signaled to continue. When the goroutine continues, the `WriteUnlocked` call outside the
lock writes `old + 1`, computed from the old value it read — overwriting the 100 `main`
wrote in between without ever seeing it. Two increments were applied (one +1, one +100),
but the result is not 101, it is 1.

This is not the running of a race condition, quite the opposite: because the order between
the two steps is fully determined by the channels, there is no timing uncertainty at all
between `main`'s write and the goroutine's write — both happen the same way, in the same
order, on every run. The race detector, looking at this code, gives no warning at all —
there is an ordering relationship established through a channel between the read and the
write, and the two goroutines never touch the same memory at the same time. The defect is
not a timing bug, it is a design bug: the lock's scope does not cover the whole place the
"protect the value" contract requires. The entire operation between reading the value and
writing it — here, computing `old + 1` and writing it back — should have stayed under the
lock; the lock protected only the moment of reading, not the moment of deciding and writing
back.

This is the lesson's limiting reading: a lock protects exactly as much as its scope, not
one line more or one line less. What needs protecting is not a single variable but the
entire operation performed on that variable — as long as the read-compute-write triple
does not stay inside the same `Lock`/`Unlock` pair, the lock's mere existence guarantees
nothing. `ProtectedCounter.Increment` gets this right: the read, the increment, and the
write happen in a single statement (`s.value++`) and inside a single lock scope.
`HalfProtected` breaks the contract by splitting the same three operations into two
separate methods, two separate lock scopes — the lock itself raises no error, because the
lock's job is only to protect its scope, not to decide whether that scope was drawn
correctly.

When `HalfProtected`'s two methods are read separately, neither looks suspicious:
`ReadAndUnlock` uses the lock correctly, and `WriteUnlocked`, looked at on its own, carries
no problem either, because it just assigns to a single field. The defect only shows up in
how the two methods are called together — there is a gap between them where the lock can
be released and taken again, and this gap cannot be seen by reading the source; it can only
be seen by asking whether another goroutine can step in between the two calls. This is a
recurring pattern when designing shared state: when the methods a type exposes are split
into separate pieces like "read" and "write," the code calling those pieces may not know
that the lock gets released in between. Keeping `ProtectedCounter.Increment` as a single
method is a deliberate choice: the contract it exposes becomes "apply one increment
atomically," and the calling side never sees the gap between reading and writing, because
no such gap exists in the source at all.

#### Lock or Channel

Throughout the previous topic, every case of sharing was set up by passing it through a
channel: a goroutine wanting to change a value sent a message to the single goroutine
holding it, and that goroutine applied the change on its own turn. This lesson does the
same job with a lock: the `ProtectedCounter` holding the value carries its own lock, and
every goroutine that wants a change accesses that memory directly, with no intermediary
goroutine standing between them. Either can produce the same result — the worker pool
pattern in the previous topic provided this through a channel, and this lesson provides the
same protection through a lock. The shared definition's third claim still holds here: what
stops a goroutine is not the channel or the lock itself, but a step taken by someone; the
choice between communication and sharing is not an escape, it is a preference.

The direction of this preference can be found with a simple question: is what is being
protected a memory location, or a piece of work in a queue? `ProtectedCounter.value` is
just an integer, and the only operation performed on it is incrementing; routing that
operation to a single owner through a channel takes more indirection than locking it
directly, without adding any extra guarantee. If the value is a more complex state — if
several fields need to stay consistent together, or an order needs to be established
between operations — a channel's single ownership draws a more natural boundary. This
lesson measures only the lock's own contract; which of the two approaches to prefer, and
when, is a broader question left for the course's closing.

#### Summary

- `sync.Mutex` is Go's lock for realizing mutual exclusion; the lines between `Lock` and
  `Unlock` form the lock's scope, and only that scope is protected.
- A goroutine waiting on a lock continues once another goroutine holding it calls
  `Unlock` — an instance of waited-for-arrived, one of the four termination classes.
- The same counter reaches 2000 through two setups, locked and unlocked; the equality is
  not proof of safety, because the unlocked setup already excludes the race by writing to
  separate memory cells instead of a single shared variable.
- A single write falling outside the lock's scope was modeled with an order forced
  through a channel: two increments were applied, but the result came out as 1 instead of
  101, because the lock protected only the read.
- A lock requires keeping the entire operation that needs protecting — reading,
  computing, writing — inside the same scope; where that scope gets drawn is a decision
  the lock itself cannot police.

#### Next Step

This lesson measured whether a single goroutine waits on a lock. But most shared-state
problems are not limited to a single waiter: waiting for an entire group of goroutines to
finish requires a different contract than waiting for each of them to separately release a
lock. The next lesson measures the tool that sets up this collective waiting — the
counter-based `sync.WaitGroup` — and its own contract: when the counter reaches zero, and
what happens if it never does.

### Wait Groups

Source: https://academia.sh/en/courses/go-concurrency/wait-groups

A counter waits for five workers to finish: Add raises the counter to five before they start, each worker guarantees its own Done with defer, and Wait returns once the counter reaches zero — the termination class is waited-for-arrived again. If the counter never reaches zero, Wait never returns, and this leak is measured with a boolean; when Done is called one time too many, the counter drops below zero and the wait group panics, the panic is caught with recover, and only panic=true is written. Limiting measurement: which of the five workers finishes first is never measured, only that all of them finish — the results are collected and sorted before being printed.

The previous lesson measured a single goroutine waiting on a single lock. But the side
protecting shared state is itself often not a single goroutine but a group: several
goroutines splitting a piece of work into shares and running it in parallel, and a main
goroutine waiting for all of them to finish. A lock does not set up this kind of waiting —
a lock only limits how many goroutines can touch a resource at the same time, it does not
count how many goroutines have finished. The tool this lesson measures, the one that does
this counting, is `sync.WaitGroup`, a wait group.

It is possible to imitate a wait group with a channel — each worker sends an empty value on
a channel once it finishes, and `main` reads exactly as many values from that channel as
there are workers. The previous topic's Worker Pools and Fan-Out and Fan-In lessons
genuinely did this: the result channel itself both carried the value and counted how many
results had arrived. A wait group separates these two jobs: a separate counter for counting
how many workers finished, and, for carrying a value (as in this lesson), a separate
channel or no channel at all. This separation needs less machinery when the workers produce
no value at all — when knowing that a job finished is enough on its own.

#### The Counter Contract

A wait group is not a slice, a channel, or a list — it is a type that holds a single
integer counter inside it. Its contract rests on three calls: `Add(n)` raises the counter
by `n`, `Done()` lowers it by one, and `Wait()` blocks until the counter reaches zero.
Using these three calls in the right order is a rule: `Add` must always be called before
the goroutine it concerns starts, `Done` must run on every exit path of every goroutine —
which is why it is written with `defer`, just like `Unlock` in the previous lesson — and
`Wait` never returns before the counter reaches zero.

Calling `Add` before the goroutine starts is not a matter of style. If `Add` were instead
called inside the goroutine itself, on its first line, `main` could reach `group.Wait()`
before that goroutine ever ran its own `Add` at all — in that case, since the counter is
still zero, `Wait` could return immediately, without ever waiting for a job that had not
even started. This is a class of bug that is hard to notice by reading the source, because
which order things run in depends on goroutine scheduling, and this lesson does not
deliberately run such a setup — because the result could vary from run to run, it would
fall under this course's determinism constraint. The rule can instead be read straight from
the source: the `group.Add(1)` line in the example sits before the `go` keyword, on every
step of the loop.

```go
// main.go — counter contract, a leak in a counter that never zeroes, a counter dropping below zero
package main

import (
	"fmt"
	"runtime"
	"sort"
	"sync"
)

func main() {
	fmt.Println("-- counter contract: Add once, Done on every path, Wait returns at zero --")
	const workerCount = 5
	var group sync.WaitGroup
	completed := make(chan string, workerCount)
	weights := []int{3, 1, 4, 1, 5}
	for i := 0; i < workerCount; i++ {
		group.Add(1)
		go func(index, weight int) {
			defer group.Done()
			sum := 0
			for j := 0; j < weight*1000; j++ {
				sum += j
			}
			completed <- fmt.Sprintf("job-%d (weight=%d, sum=%d)", index, weight, sum)
		}(i, weights[i])
	}
	group.Wait()
	fmt.Println("Wait returned: stopped, waited-for-arrived")

	close(completed)
	var reports []string
	for r := range completed {
		reports = append(reports, r)
	}
	sort.Strings(reports)
	for _, r := range reports {
		fmt.Println(r)
	}

	fmt.Println()
	fmt.Println("-- a counter that never reaches zero: Add is called, Done never is --")
	baseline := runtime.NumGoroutine()
	var leakingGroup sync.WaitGroup
	leakingGroup.Add(1)
	done := make(chan struct{})
	go func() {
		leakingGroup.Wait()
		close(done)
	}()
	select {
	case <-done:
		fmt.Println("Wait returned: this was not expected")
	default:
		fmt.Println("did Wait return: no, the counter never reached zero")
	}
	fmt.Println("is there a goroutine left that never stopped:", runtime.NumGoroutine() > baseline)

	fmt.Println()
	fmt.Println("-- the counter dropping below zero: panic --")
	panicOccurred := runAndRecover(func() {
		var overGroup sync.WaitGroup
		overGroup.Add(1)
		overGroup.Done()
		overGroup.Done()
	})
	fmt.Println("panic:", panicOccurred)
}

// runAndRecover runs the given function and reports whether it panicked; the stack trace is not printed.
func runAndRecover(fn func()) (panicOccurred bool) {
	defer func() {
		if recover() != nil {
			panicOccurred = true
		}
	}()
	fn()
	return false
}
```

```
-- counter contract: Add once, Done on every path, Wait returns at zero --
Wait returned: stopped, waited-for-arrived
job-0 (weight=3, sum=4498500)
job-1 (weight=1, sum=499500)
job-2 (weight=4, sum=7998000)
job-3 (weight=1, sum=499500)
job-4 (weight=5, sum=12497500)

-- a counter that never reaches zero: Add is called, Done never is --
did Wait return: no, the counter never reached zero
is there a goroutine left that never stopped: true

-- the counter dropping below zero: panic --
panic: true
```

**SS4.** Five workers are started with five weights (3, 1, 4, 1, 5) — one repeats twice —
and each computes its own sum and writes it to a channel. `group.Wait()` returns only after
all five have called their own `Done` — this is one more instance of the termination class
treated since the course's first lesson: `main` itself waits like a goroutine, and stops
once what it is waiting for — the counter reaching zero — arrives. The wait group here
offers a different contract than the lock: while a lock protects a single resource, a wait
group counts how many jobs have finished. The shared definition's third claim supports this
directly: the five "waited-for-arrived" lines were each set up with a separate mechanism
(an unbuffered receive, a closed channel, a lock, a wait group, a `select`), and the
dependency did not change even though the mechanism did. The previous lesson used the lock,
this lesson uses the wait group — both are members of the same family, the family of
"stopping depending on another goroutine's step."

The weights themselves were also chosen to differ on purpose: the weight `1` repeats twice
(`job-1` and `job-3`), while the remaining three (`3`, `4`, `5`) all differ. This shows that
the wait group does not distinguish between workers in any way — the counter only tracks
how many `Done` calls happened, it does not know which goroutine did which job. `job-1` and
`job-3` produce the same sum (`499500`) with the same weight, and there is no difference at
all between these two workers as far as the wait group is concerned; the distinction is
carried only by the `job-` prefixes in the output, not by the wait group itself.

**SS5.** The five workers finish at five different times — the one with a higher weight
runs a longer loop — but this lesson never measures which one finishes first. The reports
written to the channel are read from `completed` into a slice, then sorted and printed
with `sort.Strings`; the order of arrival from the channel is never printed, because that
order depends on goroutine scheduling and can vary from run to run. The only thing a wait
group guarantees is the moment the counter reaches zero; which `Done` call arrives first is
not a guarantee. This is the lesson's limiting measurement: that all five workers finished
is measured, which one finished first cannot be — had it been measured, this lesson's
output would also vary from run to run, and verification would report a mismatch.

The `group` variable is a local variable of `main`, and every goroutine accesses it through
a closure, with no pointer at all — because Go's closures capture the surrounding variables
by their own address, `group.Add`, `group.Done`, and `group.Wait` always touch the same
counter. A wait group is a struct, and the pass-by-value rule measured in the Go
Fundamentals course applies here as well: if a copy of `group` were taken and passed to a
function by value, that copy would have its own counter, independent of `main`'s own
counter — the contract would break silently. The inspector's job (the one measured in the
Go Command Family lesson) is exactly to catch patterns like this; this lesson does not
deliberately set up and run such a copy, because, like the single line falling outside the
lock's scope in the previous lesson, it would be a class of bug that is hard to notice by
reading the source.

The lock and the wait group split two separate responsibilities here. The previous
lesson's `ProtectedCounter` took on preventing more than one goroutine from writing to the
same value at the same time; whoever protects that value, it never answers the question
"did everyone finish?" The `completed` channel and `group` in this lesson do the opposite:
they count which workers finished their job, but never get involved with any memory the
workers share among themselves — the five workers here already write to no shared variable
at all, each computing its own local `sum` and sending it on the channel. In a real program
these two tools are seen together: several goroutines protect a shared value with a lock
and each calls `Done` once its job is finished, and `main` waits for all of them with
`Wait`, only reading the protected value once that happens. The lock decides what gets
protected, the wait group decides when it is ready to be read; neither question stands in
for the other.

#### A Counter That Never Reaches Zero: The Leak

**SS6.** The second setup leaves the contract half done on purpose: `leakingGroup.Add(1)`
is called, but the matching `Done` is never called in any goroutine. Because the counter
will never reach zero again, the goroutine calling `leakingGroup.Wait()` stays blocked
forever — this is the fourth termination class itself: never stopped. To check this
blocking without waiting for it, we wrap `Wait` inside a separate goroutine and a closing
channel; the `default` branch of the `select` shows, instantly and with certainty, that no
value will ever arrive from that channel — because the counter can mathematically never
reach zero. This is not a guess about timing: which branch the `select` picks does not
change on any run, because `Wait` returning depends on the counter's value, not on timing,
and that value never changes.

**SS7.** The `runtime.NumGoroutine()` call is compared against the baseline count and
produces only a `true`/`false`, not a separate count of how many goroutines are left. This
is another instance of the shared definition's second claim: not stopping is not turned
into any error or warning by the Go runtime, the rest of the program keeps running
normally — only that one goroutine never finishes. This silence belongs to the same family
as the channel leaks measured in the Goroutines lesson: neither the wait group nor a
channel produces an error on its own when the other side fails to honor its contract.

A wait group has no cancellation mechanism of its own — there is no way to stop a `Wait`
call from the outside; it waits until the counter reaches zero. The `ctx.Done()` pattern
measured in the Timeouts and Deadlines lesson fills this gap, but the wait group itself
offers no such exit; this is why, in a real program, a `Wait` call carrying the risk of an
infinite wait is usually wrapped, together with a timeout, through a separate goroutine and
channel — exactly the way it is done with the `done` channel in this lesson. This lesson
used that wrapping only to check for the leak; setting up a real timeout and cutting the
wait short was the subject of the Timeouts and Deadlines lesson, and it is not repeated
here.

#### The Counter Dropping Below Zero: Panic

**SS8.** The third setup models the opposite mistake: `Add(1)` is called once, but
`Done()` is called twice. The wait group does not silently ignore this — the moment the
counter drops negative, it panics, because a negative counter breaks the library's own
internal consistency, and Go's standard library chooses to report this immediately rather
than suppress it. `runAndRecover`, continuing the pattern of previous lessons, catches the
panic with `recover` and writes only `panic=true`/`panic=false`; the panic text itself is
never printed anywhere, because that text carries a stack trace.

Not printing the panic text is not only a concern about the stack trace. The text the
standard library gives this panic can change depending on the version that compiled the
source — there is no guarantee it stays word-for-word the same, because this text is not
part of a contract the language specifies, it is a detail of the library's internal
implementation. `panic=true`/`panic=false` instead measures the one thing the language
does guarantee: whether a panic happens when the counter drops negative, or not. Measuring
a second send to a closed channel the same way in the Closing and Ranging lesson is a
recurring habit of this course: the panic itself is measured, not the panic's text.

These two faulty setups build an interesting contrast: the counter never reaching zero
stays silent, while the counter reaching zero one too many times is reported loudly. This
difference comes from the wait group's own design, not from a judgment about which error
matters more. The counter can drop below zero because the wait group checks the counter on
every `Done` call and catches a negative value immediately; the counter never reaching
zero, on the other hand, is not something the wait group can check — the library has no way
of knowing how many `Add` calls went unanswered, because "going unanswered" is a wait that
can last forever, not an error. A panic can be caught because it is an instant event; a
leak cannot be caught because it is not an event but an unending state — it can only be
checked from the outside.

These three setups test the wait group's contract from three directions: `Wait` stops when
the counter reaches zero correctly, it never stops when the counter never reaches zero, and
it panics when the counter reaches zero one too many times. All three violations of the
contract come from the same root — the number of `Add` calls has to match the number of
`Done` calls, and it is the runtime, not the compiler, that checks this match. In the
previous lesson the lock's contract was about what had to stay under the lock; here the
contract is about how many `Add` calls correspond to how many `Done` calls. Both are rules
the compiler never sees at all: someone reading a source can only tell whether the `Add`
and `Done` calls are equal in number by watching the source run, not by reading it.

#### Summary

- `sync.WaitGroup` is a counter that works with the `Add`, `Done`, and `Wait` triple;
  `Wait` blocks until the counter reaches zero, and the termination class is
  waited-for-arrived.
- The order in which five workers finish is never measured, only that all of them
  finish; the results are collected from a channel, sorted, and printed.
- If the counter never reaches zero, `Wait` blocks forever; this was checked without
  blocking, through `select`'s `default` branch, and the leak was measured only as a
  boolean.
- An extra `Done` call that drives the counter below zero makes the wait group panic;
  the panic was caught with `recover`, and only `panic=true` was written.
- The lock's contract was about what needed protecting; the wait group's contract is
  about how many `Add` calls correspond to how many `Done` calls — both are rules the
  compiler does not check, visible only by running the source.

#### Next Step

A wait group counts a number, but on its own it protects no value and limits no operation
to a single run. Across two lessons, the lock was the only tool for protecting shared
state; the next lesson shows that this monopoly is not accurate, and measures two separate
primitives for shared state: once, which guarantees that a body runs exactly once no matter
how many goroutines call it, and atomic operations, which protect a single variable without
using a lock. Both primitives come with their own narrow contract — one protects only a
one-time setup, the other only a single variable — and where this narrowness is enough, and
where it falls short, is the next lesson's own limiting measurement.

### Concurrency Primitives

Source: https://academia.sh/en/courses/go-concurrency/concurrency-primitives

Thirty goroutines call the same initialization, but the once contract guarantees the body runs exactly one time. Thirty goroutines, each with five hundred increments, increment the same counter once with atomic operations and once with a lock; both give 15000. The limiting measurement models, with an order forced through a channel, how two separate atomic variables (balance and transaction count) can each stay consistent on their own while producing an inconsistent pair when read together: the reading side sees balance=100, transactionCount=0.

The previous lesson's closing drew a boundary: across two lessons, the lock looked like the
only tool for protecting shared state, but it is not. This lesson builds the same protection
with two separate primitives — `sync.Once`, the **once** that guarantees a body runs exactly
one time no matter how many times it is called; and `sync/atomic`, the **atomic operations**
that make operations on a single variable safe without using a lock. Both look like they do
the same job as the lock, but both carry a narrower contract than the lock: the lock could
wrap any piece of code at all, while each of these two primitives covers only one narrow
situation.

The shared definition's third claim still holds in this lesson: what stops a goroutine is not
the name of the tool, but a step taken by another goroutine. In the once, this step is the
first call finishing the body — waiting goroutines enter the waited-for-arrived class, just
as with the lock. In an atomic operation there is no waiting at all: an atomic call returns
without waiting for any goroutine to do anything, without ever blocking — a step that finishes
on its own, dependent on no one. Both represent **a single narrow form** of the lock, but one
still makes something wait, the other never does.

#### Once

A step that needs to run only once at a program's start — setting up a connection pool,
reading a configuration — can be requested by more than one goroutine at the same time.
`sync.Once` reduces this request to a single call: the function given to the `Do` method
runs **exactly one time** over its entire lifetime, no matter how many goroutines call `Do`;
every later call returns immediately without doing anything. A `sync.Once`'s zero value is
also ready to use, just like the lock: writing `var once sync.Once` is enough, no separate
setup is needed — because inside `Once` there is already a counter that tracks whether the
first call has happened, and its own lock protecting that counter. `Once` itself is a tool
built using the previous lesson's primitive.

The result does not change even if `Do` calls belonging to the same `once` variable are
given **different** functions — only the **first** call's function runs, and the functions
given to later calls are skipped without ever running. In this example this distinction does
not show, because all thirty goroutines give the same function, but the contract already
guarantees it: `Once`'s memory is not "which function was given," only a single-bit answer
to the question "was I called once already." Which goroutine's own `Do` call actually runs
the body is also not measured and cannot be measured — thirty goroutines call `Do` at the
same time, and which one "wins" depends on timing; but because all of them give the same
function, this uncertainty has no effect at all on the result, just as in the first lesson's
unlocked setup.

```go
// main.go — once, comparing an atomic counter with a locked counter, modeling two atomic variables
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	fmt.Println("-- once: N goroutines call it, the body runs once --")
	var once sync.Once
	var timesRun int64
	var initializedValue int64
	const goroutineCount = 30
	var group sync.WaitGroup
	group.Add(goroutineCount)
	for i := 0; i < goroutineCount; i++ {
		go func() {
			defer group.Done()
			once.Do(func() {
				atomic.AddInt64(&timesRun, 1)
				atomic.StoreInt64(&initializedValue, 42)
			})
		}()
	}
	group.Wait()
	fmt.Printf("how many times the body ran: %d\n", timesRun)
	fmt.Printf("initialized value: %d\n", initializedValue)

	fmt.Println()
	fmt.Println("-- does the atomic counter give the same result as the locked counter --")
	const incrementCount = 500
	var atomicCounter int64
	var group2 sync.WaitGroup
	group2.Add(goroutineCount)
	for i := 0; i < goroutineCount; i++ {
		go func() {
			defer group2.Done()
			for j := 0; j < incrementCount; j++ {
				atomic.AddInt64(&atomicCounter, 1)
			}
		}()
	}
	group2.Wait()

	var lock sync.Mutex
	lockedCounter := 0
	var group3 sync.WaitGroup
	group3.Add(goroutineCount)
	for i := 0; i < goroutineCount; i++ {
		go func() {
			defer group3.Done()
			for j := 0; j < incrementCount; j++ {
				lock.Lock()
				lockedCounter++
				lock.Unlock()
			}
		}()
	}
	group3.Wait()

	fmt.Printf("atomic counter : %d\n", atomicCounter)
	fmt.Printf("locked counter : %d\n", lockedCounter)
	fmt.Printf("are they equal : %v\n", atomicCounter == int64(lockedCounter))

	fmt.Println()
	fmt.Println("-- limiting measurement: modeling the joint consistency of two atomic variables --")
	var balance, transactionCount int64
	step1 := make(chan struct{})
	done := make(chan [2]int64)

	go func() {
		<-step1
		readBalance := atomic.LoadInt64(&balance)
		readTransactions := atomic.LoadInt64(&transactionCount)
		done <- [2]int64{readBalance, readTransactions}
	}()

	atomic.AddInt64(&balance, 100)
	step1 <- struct{}{}
	interim := <-done
	atomic.AddInt64(&transactionCount, 1)

	fmt.Printf("when the writer finished balance=%d transactionCount=%d\n",
		atomic.LoadInt64(&balance), atomic.LoadInt64(&transactionCount))
	fmt.Printf("what the reader saw in between   balance=%d transactionCount=%d (inconsistent pair)\n",
		interim[0], interim[1])
}
```

```
-- once: N goroutines call it, the body runs once --
how many times the body ran: 1
initialized value: 42

-- does the atomic counter give the same result as the locked counter --
atomic counter : 15000
locked counter : 15000
are they equal : true

-- limiting measurement: modeling the joint consistency of two atomic variables --
when the writer finished balance=100 transactionCount=1
what the reader saw in between   balance=100 transactionCount=0 (inconsistent pair)
```

**SS9.** Thirty goroutines make the same `once.Do(...)` call at nearly the same time, but
`timesRun` is only `1`. `sync.Once`'s contract works differently here than the wait group's
did: the wait group relied on calls that raised and lowered a counter **from the outside**,
while `Once` decides **on its own**, internally, that once it has run once it will never run
again. Only one of the thirty calls actually runs the body; the remaining twenty-nine wait
for the moment `Do` returns — immediately if the body already ran, or until the body
finishes if it is still running. This is another mechanism of the waited-for-arrived class:
the waiting goroutines do not stop on their own; the first call finishing the body sets them
free.

`initializedValue`'s output shows the second half of this guarantee: it is not enough for
the body to run **once**; the value the body writes also has to be visible to **every**
goroutine. Only one of the thirty goroutines writes `42` to `initializedValue`, but every
goroutine returning from `Do` — whether its own call ran the body or not — reads the same
`42`. This is a consequence of `sync.Once`'s own internal synchronization: returning from
`Do` carries not only the information "the body ran somewhere," but every value the body
wrote along with it.

**SS10.** The second measurement produces the same total through two separate paths:
`atomicCounter` is protected with `atomic.AddInt64` and touches no lock at all;
`lockedCounter` is protected with a `Lock`/`Unlock` pair, as in the previous lesson. Thirty
goroutines, each with five hundred increments, apply fifteen thousand increments in total,
and both counters reach exactly `15000`. This equality differs from the first lesson's
locked/unlocked comparison: there, the unlocked setup never touched a single shared memory
location at all, while here `atomicCounter` genuinely is a single shared memory location and
thirty goroutines genuinely write to it at the same time. An atomic operation excludes the
shared-state race **not by not running it, but by making it safe** — through the indivisible
read-modify-write operation the hardware provides, with no lock required.

The atomic operation concept closed in **M01/K05 Operating System Concepts** applies here
without change: an operation being indivisible means no other goroutine can step in between.
Go's `sync/atomic` package provides this indivisibility, backed by hardware, for a specific
set of integer and pointer types; the `AddInt64` call itself is never left half-done, and no
goroutine can ever step in between its read and its write.

This guarantee comes at a cost: what `sync/atomic` protects is only a specific handful of
types — `int32`, `int64`, `uint32`, `uint64`, pointers — and every call operates on only
**one** of these types. The lock defined no such boundary — a struct, a slice, a map, or an
arbitrary-length piece of code updating more than one variable could all sit between a
`Lock`/`Unlock` pair. The atomic operation's narrow contract is exactly the price of giving
up part of that flexibility: protecting a single integer needs no lock at all, but the
moment what needs protecting goes beyond a single integer, the atomic operation falls short.

Alongside `AddInt64` and `LoadInt64`, used in this lesson, `sync/atomic` also carries a
compare-and-swap call: it writes the new value if the given variable equals the expected
value, and reports this without doing anything otherwise — another indivisible operation no
goroutine can step into between the read and the write. This lesson does not run that call;
what matters here is that `sync/atomic` offers every operation on a single integer (adding,
reading, writing, conditional replacement) with the same indivisibility guarantee, but what
it offers always stays limited to **one** variable.

#### Limiting Measurement: Holding Two Atomic Variables Together

**SS11.** The third setup uses two variables — `balance` and `transactionCount` — instead of
one, and both are read and written atomically on their own. But there is no joint protection
between them: the writing side updates `balance` first, then `transactionCount`; the moment
left between these two updates can show an outside-looking goroutine an **inconsistent
pair**. This gap is modeled with an order forced through a channel: the reading goroutine
waits for a signal on the `step1` channel, `main` sets `balance` to 100 and sends the signal
first, and the reading goroutine reads both `balance` and `transactionCount` at exactly this
moment — `balance=100` but `transactionCount` is still `0`. Only after this does `main`
increment `transactionCount`.

This is a boundary in the same family as the lock scope model from the previous lesson, but
it sits in a different place: there, the problem was the lock's **scope** being drawn
incorrectly; here, the two variables have **no shared scope at all** — each is atomic on its
own, but not the two together. `balance` carries a defined value the moment it is read, and
`transactionCount` also carries a defined value the moment it is read; the reading result is
not **wrong**, only that a write by another goroutine can slip in between the two reads, and
this can turn two individually correct values into a false story when taken together. An
atomic operation exists to protect a single variable; when more than one variable needs to
stay consistent **together**, an atomic operation is not enough, and a lock takes that
protection back over.

This boundary itself is not a design flaw, but a generalization of the previous lesson's
lock scope example. There, the problem was a single counter's read-compute-write triple
spilling outside the lock; here, two separate counters are **not both** under a single
scope. Both ask the same question: where does the operation that needs protecting begin and
end? Because `balance` and `transactionCount` are atomic separately, this question cannot be
noticed by looking at the source — both variables look correct on their own line, and the
fact that the relationship between them is not protected only shows up once a piece of code
trying to read the two variables **together** is written and run.

#### Lock, Once, and Atomic Operations

The three tools measured across three lessons give three separate narrow answers to the
same question. The lock answers "which piece of code should run under exactly one goroutine
at a time?" and draws this scope itself, through the `Lock`/`Unlock` pair — if the scope is
drawn wrong (the first lesson's limiting measurement), the lock does not notice. Once
answers "has this body already run?" and remembers this answer forever; a second `Do` call
never runs the body again, because the question was already answered once. An atomic
operation answers "how many goroutines can touch this single variable at the same time?" and
guarantees the answer at the hardware level, but only asks the question for **one**
variable.

All three protect shared state, but none of the three's scope stands in for the others: it
is not possible to keep protecting a counter continuously with a once, nor to make more than
one step look like a single operation with an atomic operation. Which primitive gets chosen
in a codebase reveals the nature of what needs protecting — whether it is an event, a single
value, or an arbitrary piece of code. This is the shared conclusion of these three lessons:
there is no single correct way to protect shared state, only three separate narrow contracts
that vary with the shape of what needs protecting.

These three narrow contracts can also nest inside one another. `sync.Once` itself was built
with a lock; a type like the previous lesson's `ProtectedCounter` can carry its own lock
while also carrying a `sync.Once` field, initializing a field only on first access. An
atomic operation does not **replace** the lock either — it leaves the narrowest situations,
the ones a lock could stand in for, without a lock. All three are one view of the same
measurement axis: how a goroutine accesses shared state is measured by how much of that
state it holds under protection, and for how long.

#### Summary

- `sync.Once` guarantees that the body given to `Do` runs exactly one time no matter how
  many goroutines call it; only one of thirty calls ran the body.
- Waiting goroutines stay blocked until `Once`'s body finishes — a mechanism of the
  waited-for-arrived class distinct from the lock's and the wait group's.
- `sync/atomic` protects operations on a single variable with the hardware's indivisible
  read-modify-write support; thirty goroutines applied fifteen thousand increments, and the
  atomic counter gave the same result (`15000`) as the locked counter.
- Two separate atomic variables, even though each stays consistent on its own, can produce
  an inconsistent pair when read together; this was modeled with an order forced through a
  channel, not run.
- An atomic operation protects a single variable; when more than one variable needs to stay
  consistent together, lock scope is still the tool required.
- The lock, wait group, once, and atomic operation measured across three lessons are four
  separate views of the same measurement axis: each is defined by how much of the shared
  state it holds under protection, and for how long.

#### Next Step

Across three lessons, shared state was protected with a lock, a wait group, and now once and
atomic operations — but every measurement knew from the start that the protection
**existed**, because we wrote the source. None of these three lessons ran an actual race
condition: the first lesson excluded it with separate memory and a forced model, this lesson
excluded it with a forced model as well. In a real codebase, who says whether protection was
forgotten, without reading the source? The course's final lesson asks this question of the
race condition detector that ships with the language's own distribution: an unprotected
share, a locked share, and a transfer through a channel are all run through the same
detector, the detector's answer (found or not found) is read, and the detector's own
boundary — its inability to see a race in a path that never runs — emerges as the course's
final measurement.

### Race Detector

Source: https://academia.sh/en/courses/go-concurrency/race-detector

The detector is a member of the language's own distribution and is called from within the program: a fabricated source is written to a temporary directory and run with the detector option, and only the exit code is read. Of three setups, it finds the unprotected share, and does not find the locked share or the transfer through a channel. The limiting measurement — the course's final and most important boundary — shows that the same unprotected code, placed inside a branch that never runs, is also not found by the detector: the detector sees only the path that runs.

Across three lessons, shared state was protected with a lock, a wait group, once, and atomic
operations — but every measurement knew from the start that the protection **existed**,
because we wrote the source. In a real codebase, who says whether protection was forgotten,
without reading the source? This final lesson asks this question of the **race condition
detector** that ships with the language's own distribution.

This course's second hard constraint required, across three lessons, that a race condition
never actually be run — excluded either by modeling it or by separate memory. In this final
lesson, this constraint reverses: measuring the detector itself requires giving it a
fabricated race it can genuinely catch. What is measured is still not the race's
**outcome** — what value `counter` reaches is never read anywhere — only the detector's
single-bit answer: found, or not found.

#### How the Detector Is Called from Within the Program

The detector is a member of the language's own distribution, like the compiler, the
formatter, and the inspector measured in the Go Command Family lesson, and it is called
through the same procedure: fabricated source is written to a temporary module, the command
runs in that directory, the result is read. The difference is in what the result being read
is — here neither an output text nor a format difference is read, only the **exit code**.
When the detector finds a race, the exit code is nonzero; when it does not, the program
closes normally with zero.

Unlike the compiler and the inspector, the detector does not just read the source and make a
decision; it **runs** the source and, while running, watches every memory read and write.
This watching is given to the call itself as an option — the command normally runs the
program it executes without watching it at all, and when given the detector option it runs
with an observer attached to every goroutine's every memory access. This observation comes
at a cost: the same program runs slower than normal and uses more memory while running with
the detector, because who accessed what, and when, is separately recorded for every access.
This lesson does not measure that cost at all — the course's ban on duration still applies
here — it only reads the binary answer the detector gives.

```go
// tools/tools.go — a shared routine that calls the race detector from within the program
package tools

import (
	"os"
	"os/exec"
	"path/filepath"
)

// DetectorFound runs the given source in a temporary module with the detector; the
// detector's exit code is nonzero when it finds a race. The detector's own output is
// written nowhere.
func DetectorFound(source string) bool {
	dir, _ := os.MkdirTemp("", "trial")
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "main.go"), []byte(source), 0o644)

	cmd := exec.Command("go", "run", "-race", ".")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() != nil
}
```

`DetectorFound` deliberately never reads its own output: `cmd.Stdout` and `cmd.Stderr` are
never assigned, so everything the detector writes goes to the null device. This rests on the
same reasoning as the panic text not being printed in previous lessons — the detector's
output carries real memory addresses, real goroutine numbers, and a stack trace; the only
thing measured is the error `cmd.Run()` returns, that is, whether the command's **exit code
was zero or not**. The subprocess is called with `GOTOOLCHAIN=local`, exactly as in the Go
Command Family lesson — so that no toolchain gets pulled down from the network.

#### Three Setups, the Detector's Answer

```go
// main.go — the detector's answer across three setups, and the invisibility of a race in a path that never runs
package main

import (
	"fmt"

	"lesson/tools"
)

func main() {
	unprotected := `package main

func main() {
	var counter int
	done := make(chan struct{}, 2)
	go func() { counter++; done <- struct{}{} }()
	go func() { counter++; done <- struct{}{} }()
	<-done
	<-done
}
`

	locked := `package main

import "sync"

func main() {
	var counter int
	var lock sync.Mutex
	done := make(chan struct{}, 2)
	go func() {
		lock.Lock()
		counter++
		lock.Unlock()
		done <- struct{}{}
	}()
	go func() {
		lock.Lock()
		counter++
		lock.Unlock()
		done <- struct{}{}
	}()
	<-done
	<-done
}
`

	viaChannel := `package main

func main() {
	channel := make(chan int)
	go func() { channel <- 1 }()
	<-channel
}
`

	unreachablePath := `package main

func main() {
	var counter int
	done := make(chan struct{}, 2)
	unreachableBranch := false
	if unreachableBranch {
		go func() { counter++; done <- struct{}{} }()
		go func() { counter++; done <- struct{}{} }()
		<-done
		<-done
	}
	_ = counter
}
`

	fmt.Println("-- the detector's answer across three setups --")
	fmt.Printf("unprotected share : found=%v\n", tools.DetectorFound(unprotected))
	fmt.Printf("locked share      : found=%v\n", tools.DetectorFound(locked))
	fmt.Printf("channel transfer  : found=%v\n", tools.DetectorFound(viaChannel))

	fmt.Println()
	fmt.Println("-- limiting measurement: a race in a path that never runs is invisible --")
	fmt.Printf("race in the unreachable path : found=%v\n", tools.DetectorFound(unreachablePath))
}
```

```
-- the detector's answer across three setups --
unprotected share : found=true
locked share      : found=false
channel transfer  : found=false

-- limiting measurement: a race in a path that never runs is invisible --
race in the unreachable path : found=false
```

**SS12.** The three sources differ from each other only in the form the protection takes.
`unprotected` is two goroutines writing to the same `counter` with no protection at all —
the unlocked version of the first lesson's `ProtectedCounter`, except this time it genuinely
writes to a single genuinely shared memory location at the same time. `locked` wraps the
same write with a `sync.Mutex`. `viaChannel` touches no shared memory at all, passing the
value through a channel instead. The detector gives `true` only for the first — the locked
share and the channel transfer, through two separate methods, both come out clean in the
detector's eyes.

`viaChannel` coming out clean confirms a pattern established since this course's first
lesson: a value crossing a channel is copied, and the sending side never touches it again
after sending, so a situation where two goroutines access the same memory at the same time
is never set up at all. `locked` coming out clean is a direct confirmation of the first
lesson as well: the single line taken between a `Lock`/`Unlock` pair genuinely prevents two
goroutines from running at the same time, as the detector itself now confirms. What was
described in writing across three lessons is confirmed here by the detector's own
observation.

These three results are the one place where the race excluded by modeling it or by separate
memory across three lessons finally, genuinely exists. Unlike the first lesson's unlocked
counter, the `unprotected` setup uses only two goroutines instead of thirty separate ones,
and both enter the same `counter++` operation at the same time — the three separate steps of
reading, incrementing, and writing can be interrupted in between. The detector catches this
interruption by watching every memory access at runtime; because no such interruption ever
happens in the locked and channel setups, the detector finds nothing, reporting not that the
source is "clean," but that **it saw no violation in this run**.

#### Limiting Measurement: The Detector Sees the Path That Runs

**SS13.** `unreachablePath` carries exactly the same race as `unprotected` — the same two
goroutines, the same unprotected `counter++`. The only difference is that these goroutines
are placed inside a condition named `unreachableBranch` that is never `true`. The detector
answers `found=false` for this source too — the source **does** carry a race, but the
detector cannot find it, because those goroutines never **started** in this run. The
detector does not read the source and perform a hazard analysis; it only observes the
running program, and it cannot see a memory access it never observed, in any way at all.

This is the course's closing boundary: the detector is not a tool of **proof**, but of
**observation**. A source passing the detector with `found=false` does not say there is no
race in that source, only that no race was seen **in any of the paths that ran in that
run**. `unreachablePath` shows this difference most clearly — the source carries exactly the
same hazard, but the answer flips, because the hazardous branch was never selected in this
run. In a codebase, a rarely entered branch, an error-handling path, or a branch tied to a
configuration can carry a race the detector never sees — for as long as tests never run that
branch.

This boundary does not make the detector unnecessary, it only narrows what it guarantees.
When the detector says `found=true`, this is definite proof — as with the fabricated
`unprotected` source, a violation was genuinely observed. But `found=false` is not a
guarantee, only a report of that one run. This falls under the same boundary as testing's
general nature: a test passing does not prove the code is correct, only that the paths
tested gave the expected behavior in that run — the detector establishes this same boundary
once more for concurrency: a branch that never runs, no matter how well it is written, has
never been tested at all.

#### Course Wrap-Up

Across fourteen lessons, this course had a single question: how does a goroutine stop? The
shared definition's three claims gave three answers to this question. First: starting a
goroutine does not mean it will finish — only two of twelve goroutines stopped on their own,
the rest depended on a step taken by another goroutine. Second: not stopping produces no
error — all three leaks were silent, the program closed with a normal exit. Third, the one
specific to this course: what stops a goroutine is not the channel itself, but **a step
taken by someone** — the lock, the wait group, once, all of them build the same dependency
with a different mechanism.

A limiting reading and a blind spot complete these three claims. The limiting reading:
"stopping on its own" is also a contract, because a goroutine whose body has finished does
not announce this to anyone on its own — announcing it still requires a channel or a
counter. The blind spot: the **count** of a leak was never measured in any lesson, only its
**presence**, as a boolean, because the runtime's own goroutine count could be compared
against a baseline value, not measured as a true count.

This final lesson adds a fourth observation: it is hard to say whether protection **exists**
by reading the source, but the detector does not say this exactly either — it only says
whether it saw a violation **on the path it ran**. The course closes without overreaching:
these fourteen lessons do not cover the whole of Go's concurrency model, only give
measurable answers, through four mechanisms and one detection tool, to how a goroutine stops
and how a share gets protected.

| Lesson | Way of stopping measured | Class (on its own · waited-for-arrived · cancelled · never stopped) | Limiting measurement |
|---|---|---|---|
| Goroutines | Twelve goroutines, four ways of stopping (2/5/2/3 distribution) | on its own · waited-for-arrived · cancelled · never stopped (all) | "Stopped on its own" is also a contract — announcing it requires something |
| Channels | Send-receive rendezvous on an unbuffered channel | waited-for-arrived | A channel is not a queue, the value is copied |
| Buffered and Unbuffered Channels | Blocking deferred according to capacity | waited-for-arrived | The buffer defers synchronization, it does not remove it |
| `select` Statement | Selecting the ready one among multiple channels | waited-for-arrived | Selection among simultaneously ready branches is random, modeled without running |
| Channel Closing and Ranging | A receive from a closed channel giving the zero value and `false` | waited-for-arrived | Sending to a closed channel and closing it a second time both panic, only `panic=true/false` is written |
| Worker Pools | Workers stopping once the job channel closes | waited-for-arrived | Adding more workers does not split the work; a channel that never closes leaks |
| Fan-Out and Fan-In | Fanned-out work rejoining on the fan-in channel, the reason the collector stops | waited-for-arrived | Only one goroutine can safely close the fan-in channel; if it is never closed, the collector never stops |
| Pipelines | Each stage keeping the chain going by opening and closing its own channel | waited-for-arrived | When the consumer gives up early, the stages ahead of it never stop |
| The Context Package | A single cancellation reaches every goroutine waiting on `ctx.Done()` at the same time | cancelled | Context does not forcibly stop anyone, it only notifies; a goroutine that never checks `Done()` keeps doing its work |
| Timeouts and Deadlines | A timeout is a `select` branch choice; what is measured is the branch chosen, not the duration | cancelled (the ready branch) | It rescues the waiter but does not stop the waited-for work; the worker goroutine keeps running, and the leak boolean shows this |
| Mutexes | The waiter continuing once the lock is released | waited-for-arrived | The lock protects only its scope; anything outside the scope stays unprotected |
| Wait Groups | `Wait` returning once the counter reaches zero | waited-for-arrived | Order is not guaranteed, only that all of them finished is guaranteed |
| Concurrency Primitives | Once running the body exactly one time | waited-for-arrived | An atomic operation protects a single variable; the two together were modeled without running |
| Race Detector | The detector's answer across three setups | on its own (unprotected/locked) · waited-for-arrived (channel) | The detector sees only the path that runs, it cannot find a race in a path that does not run |

#### Summary

- The detector is a member of the language's own distribution and is called from within
  the program; a fabricated race was run in a temporary module with the detector option,
  and only the exit code was read.
- Of the three setups, only the unprotected share gave `found=true`; the locked share and
  the transfer through a channel both came out clean in the detector's eyes — directly
  confirming the distinction described in writing across three lessons.
- When the same unprotected source was placed inside a branch that never runs, the detector
  could not find it: the detector is not proof, only an observation of the paths that ran
  in that run.
- The shared definition's three claims — stopping on one's own is the minority, not
  stopping is silent, what stops a goroutine is a step taken by someone — were confirmed
  across fourteen lessons through four separate mechanisms.
- The course gathered the answers measured by all fourteen lessons into a single table;
  every row was read from its own source lesson.

