Notes — Module 9: Concurrency¶
These are your personal study notes. Write freely and honestly. Incomplete notes are fine — they show where your understanding still needs work. Return to this file to add insights as they develop over time.
Module: [[go/9. Concurrency]] Topic: [[go]] Date started: YYYY-MM-DD Status: In progress
Concept Map¶
Sketch how the concepts in this module relate to each other. Fill in the Mermaid diagram.
mindmap
root((Concurrency))
goroutines
go keyword
cheap (2KB stack)
main returns kills all
per-goroutine panic recovery
channels
unbuffered (rendezvous)
buffered (async)
direction types chan<- and <-chan
close and range
comma-ok receive
nil channel blocks forever
select
multiplexing
default for non-blocking
time.After for timeouts
sync package
WaitGroup (wait for N goroutines)
Mutex (exclusive lock)
RWMutex (many readers / one writer)
Once (one-time initialization)
data races
race detector -race flag
mutex fix
atomic fix
deadlocks
all goroutines asleep message
prevention rules
philosophy
communicate don't share memory
when mutex is better
patterns
generator
fan-out / fan-in
deeper patterns in Module 12
Alternative: draw this on paper, photo it, and link the image here.
Key Insights¶
The "aha moments" — the things that, once understood, made the rest clear. Be specific: "I finally understood X because Y" is more useful than "X makes sense".
- Goroutines are not threads: I finally understood that goroutines are managed by the Go runtime, not the OS. A single OS thread can run thousands of goroutines. The scheduler (details in Module 17) is what makes goroutines cheap.
- Unbuffered channels are rendezvous points: Not just "slow channels" — they guarantee that the send and receive happen at the same moment. The sender and receiver must both be ready. This is actually stronger synchronization than a buffered channel.
- Add insights as you discover them
My Understanding¶
Explain the core concepts in your own words, as if teaching them to someone else. If you can't explain it simply, you don't understand it well enough yet.
Goroutines¶
Your explanation here
What I'm still unsure about: (e.g., exactly when the Go scheduler preempts a goroutine — see Module 17)
Channels (unbuffered vs buffered)¶
Your explanation here
What I'm still unsure about: (e.g., what happens if I range over a channel that nobody ever closes)
select¶
Your explanation here
What I'm still unsure about: (e.g., if two cases are ready simultaneously, is the selection truly random or biased by implementation?)
sync.WaitGroup¶
Your explanation here
What I'm still unsure about: (e.g., whether I can reuse a WaitGroup after Wait returns)
Data races and the race detector¶
Your explanation here
What I'm still unsure about: (e.g., what "happens before" means formally in the Go memory model — see Module 17)
Connections to Other Topics¶
How does this module connect to things you already know?
| This module's concept | Connects to | How |
|---|---|---|
| Channels as CSP primitives | [[concurrency]] | Go's channel model is an implementation of Hoare's CSP; the shared-concept page has a broader comparison |
| WaitGroup + channel for result collection | [[go/8. Error Handling]] | Goroutines need to propagate errors back to the caller; the (result, error) channel pair is the standard idiom |
| Goroutine scheduler internals | [[go/17. Runtime Internals and the Memory Model]] | The M:N scheduler, GOMAXPROCS, and the Go memory model explain exactly what goroutines guarantee |
Questions That Arose¶
Log questions as they appear. Don't stop to answer them now — just capture them. Then move the serious ones to QUESTIONS.md.
- What exactly does the runtime do when it detects "all goroutines are asleep"? → added to QUESTIONS.md as Q001
- Can I range over a nil channel? → might be answered by experimenting (nil channel blocks forever, so range would hang)
- Does
sync.Once.Dowork correctly if the initialization function itself spawns goroutines?
Code Snippets Worth Remembering¶
Patterns, idioms, or examples that captured something important.
WaitGroup + defer Done¶
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done() // always via defer — runs even on panic
doWork()
}()
wg.Wait()
Why I'm saving this: The canonical goroutine launch pattern. wg.Add before launch, wg.Done via defer inside.
Mutex protecting a struct field¶
type SafeMap struct {
mu sync.Mutex
m map[string]int
}
func (s *SafeMap) Set(k string, v int) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[k] = v
}
Why I'm saving this: The standard mutex-in-struct pattern. Always use defer for Unlock so early returns don't leave the mutex locked.
select with timeout¶
select {
case result := <-resultCh:
return result, nil
case <-time.After(500 * time.Millisecond):
return "", errors.New("timed out")
}
Why I'm saving this: The canonical timeout pattern. time.After returns a channel that fires after the duration — clean and idiomatic.
Buffered channel in fire-and-forget timeout pattern¶
resultCh := make(chan string, 1) // buffered — goroutine won't leak if we time out
go func() {
resultCh <- slowWork() // can send even if caller already timed out
}()
Why I'm saving this: Without the buffer, a goroutine that finishes after the timeout would block on the send and leak. The buffer of 1 lets the goroutine complete cleanly.
What Tripped Me Up¶
Mistakes I made, misconceptions I had, things that confused me more than they should have.
- Loop variable capture — I initially thought each goroutine would capture its own copy of the loop variable. They all share the same variable. Passing
ias an argument (or using Go 1.22) is the fix. - WaitGroup.Add inside the goroutine — I put
wg.Add(1)as the first line of the goroutine body. This is a race —wg.Wait()could return before the goroutine runs. Add must be in the parent.
Summary in My Own Words¶
Write a 3–5 sentence summary of this entire module without looking at any notes. If you can't do this, you need more study time.
Write your summary here after completing the module.
Last updated: YYYY-MM-DD