Go's concurrencyWhat is concurrency?The ability of a program to handle multiple tasks at the same time, like serving thousands of users without slowing down. model is built on CSP (Communicating Sequential Processes), the idea that goroutines should coordinate by passing messages, not by sharing memory. Channels are those message pipes. They're type-safe, goroutineWhat is goroutine?A lightweight concurrent function in Go launched with the go keyword - much cheaper than an OS thread (~2KB vs ~1MB of stack).-safe, and the primary way concurrent Go code communicates.
AI generates channelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue). code prolifically, and gets it wrong in subtle ways that compile fine but deadlockWhat is deadlock?A situation where two or more operations are stuck waiting on each other forever, so none of them can proceed. at runtimeWhat is runtime?The environment that runs your code after it's written. Some languages need a runtime installed on the machine; others (like Go) bake it into the binary..
ChannelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue). basics
A channel is a typed conduit. You create it with make, send with <-, and receive with <-:
ch := make(chan string) // unbuffered channel of strings
go func() {
ch <- "hello" // send - blocks until someone receives
}()
msg := <-ch // receive - blocks until someone sends
fmt.Println(msg) // "hello"The arrow direction tells you everything: ch <- value pushes into the channel, <-ch pulls out.
| Operation | Syntax | Blocks when |
|---|---|---|
| Create unbuffered | make(chan T) | N/A |
| Create buffered | make(chan T, n) | N/A |
| Send | ch <- value | Buffer full (or unbuffered and no receiver) |
| Receive | v := <-ch | Buffer empty (or unbuffered and no sender) |
| Receive + check | v, ok := <-ch | Buffer empty and channel open |
| Close | close(ch) | N/A (never blocks) |
Unbuffered vs buffered: the critical distinction
This is where AI fails most often. The difference isn't just performance, it fundamentally changes program behavior.
Unbuffered channels are synchronous rendezvous points. The sender blocks until a receiver is ready, and vice versa. Both goroutines must arrive at the channelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue). operation at roughly the same time:
ch := make(chan int) // unbuffered
// This deadlocks - no one is receiving!
ch <- 42 // blocks forever in main goroutine
fmt.Println(<-ch)Buffered channels decouple sender and receiver. The sender only blocks when the buffer is full:
ch := make(chan int, 3) // buffer of 3
ch <- 1 // doesn't block - buffer has space
ch <- 2 // doesn't block
ch <- 3 // doesn't block
// ch <- 4 // WOULD block - buffer full
fmt.Println(<-ch) // 1 (FIFO)| Aspect | Unbuffered | Buffered |
|---|---|---|
| Synchronization | Sender and receiver synchronized | Decoupled up to buffer size |
| Deadlock risk | High if used in same goroutine | Lower but still possible |
| Backpressure | Immediate, sender feels it instantly | Delayed, only when buffer fills |
| Use case | Handoffs, signaling, synchronization | Work queues, rate smoothing |
| AI default | Rarely chosen correctly | Often over-buffered to "fix" deadlocks |
ChannelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue). directions in function signatures
Go lets you restrict a channel parameter to send-only or receive-only. This is documentation and compile-time safety:
func producer(out chan<- int) { // can only send
out <- 42
// <-out // compile error
}
func consumer(in <-chan int) { // can only receive
val := <-in
// in <- 1 // compile error
fmt.Println(val)
}When you ask AI to write a pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production. function, check that it uses directional channels. If all channels are bidirectional chan T, the code works but loses compile-time safety against accidental misuse.
chan T (bidirectional) from functions that should return <-chan T (receive-only). This leaks control, callers can close or send on a channel they shouldn't touch.Closing channels
Closing a channelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue). signals that no more values will be sent. Receivers can detect this:
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // signal: no more values
}()
// range automatically stops when channel is closed
for v := range ch {
fmt.Println(v) // 0, 1, 2, 3, 4
}The rules around closing are strict and unintuitive:
| Action | Result |
|---|---|
| Receive from closed channel | Returns zero value + false for ok |
| Send on closed channel | panic |
| Close already-closed channel | panic |
| Close nil channel | panic |
| Receive from nil channel | Blocks forever |
| Send on nil channel | Blocks forever |
close(ch), the second one panics. The rule: exactly one goroutine owns closing a channel, and it's always the sender (or a coordinator that knows all senders are done).The deadly deadlockWhat is deadlock?A situation where two or more operations are stuck waiting on each other forever, so none of them can proceed. patterns
AI generates these constantly. Learn to spot them:
// DEADLOCK 1: unbuffered send and receive in same goroutine
func main() {
ch := make(chan int)
ch <- 1 // blocks forever - no other goroutine to receive
fmt.Println(<-ch)
}
// DEADLOCK 2: two goroutines waiting on each other
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
val := <-ch1 // waits for ch1
ch2 <- val // never reaches here
}()
val := <-ch2 // waits for ch2 - circular dependency
ch1 <- val
}
// DEADLOCK 3: forgetting to close a channel with range
func main() {
ch := make(chan int)
go func() {
ch <- 1
ch <- 2
// forgot close(ch)!
}()
for v := range ch { // blocks forever after receiving 2
fmt.Println(v)
}
}range loop blocks forever waiting for more values. Every pipeline stage that returns a channel must close it when done.ChannelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue). patterns you'll see AI generate
Signaling with empty struct
When you only need to signal an event (no data), use chan struct{}, zero memory per signal:
done := make(chan struct{})
go func() {
doWork()
close(done) // signal completion to all receivers
}()
<-done // wait for signalClosing a channel unblocks all receivers simultaneously. This is how you broadcast "done" to multiple goroutines.
Generator pattern
A function that returns a receive-only channel:
func fibonacci(n int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
a, b := 0, 1
for i := 0; i < n; i++ {
ch <- a
a, b = b, a+b
}
}()
return ch
}
for v := range fibonacci(10) {
fmt.Println(v)
}context.Context for cancellation.