Do not communicate by sharing memory; instead, share memory by communicating. Goroutines and Channels form the core primitives of Go’s concurrency model.
TL;DR (Quick Summary)#
goKeyword: Spawns a new concurrent Goroutine starting at ~2KB memory.- Unbuffered Channels (
make(chan T)): Synchronous handoff. Sender blocks until receiver receives. - Buffered Channels (
make(chan T, N)): Asynchronous buffer. Sender blocks ONLY when buffer is full (len == N). - Channel Directions:
chan<- T(Send-only),<-chan T(Receive-only). Enforces safety in function signatures. selectStatement: Multiplexes multiple channel operations, executing whichever case is ready first.
1. Unbuffered vs Buffered Channel Flow#
graph LR
subgraph Unbuffered Channel (Sync Handoff)
Sender1["Goroutine A: ch <- val"] -->|Blocks until Receiver ready| SyncChan["Unbuffered chan int"]
SyncChan --> Receiver1["Goroutine B: <-ch"]
end
subgraph Buffered Channel (Async Buffer: Cap 2)
Sender2["Goroutine A: ch <- val"] -->|Fills Buffer instantly| BufChan["'[ Val 1 | Val 2 "]"]
BufChan --> Receiver2["Goroutine B: <-ch"]
end
2. Step-by-Step Lab: Channels and select Multiplexing#
package main
import (
"fmt"
"time"
)
// Channel Direction: Send-Only channel parameter (chan<- string)
func fetchAPI1(ch chan<- string) {
time.Sleep(100 * time.Millisecond)
ch <- "Response from API 1"
}
// Channel Direction: Send-Only channel parameter
func fetchAPI2(ch chan<- string) {
time.Sleep(200 * time.Millisecond)
ch <- "Response from API 2"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go fetchAPI1(ch1)
go fetchAPI2(ch2)
// Multiplex channel reads using select
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
case <-time.After(500 * time.Millisecond):
fmt.Println("Timeout waiting for API response!")
}
}
}3. Comparison: Unbuffered vs Buffered Channels#
| Property | Unbuffered Channel | Buffered Channel |
|---|---|---|
| Declaration | make(chan T) | make(chan T, capacity) |
| Capacity | 0 | capacity > 0 |
| Send Blocking | Blocks until receiver reads. | Blocks ONLY when buffer is full. |
| Synchronization | Guarantees exact point-in-time handshake. | Decouples sender and receiver execution timing. |
4. Troubleshooting & Common Errors#
Error 1: Send on Closed Channel Panic#
The Cause: Attempting to execute ch <- val on a channel that has already been closed.
close(ch)
ch <- 42 // 💥 panic: send on closed channelThe Fix: Always enforce a single owner pattern: only the sender should close a channel, never the receiver!
Summary & Next Steps#
In this episode:
- We spawned lightweight Goroutines with the
gokeyword. - We passed messages between Goroutines using Unbuffered and Buffered channels.
- We multiplexed channel reads using
select.
In Episode 11: Advanced Concurrency & Worker Pools, we will master sync.WaitGroup, sync.Mutex, and building scalable Worker Pools!

