Channels
Unbuffered/buffered, select, closing, worker pool
Channel Basics
A channel is a typed synchronous queue for safely passing values between goroutines. An unbuffered channel is a "rendezvous point": both sender and receiver must arrive before either can proceed.
package main
import "fmt"
func main() {
ch := make(chan int)
go func() { ch <- 42 }()
v := <-ch
fmt.Println(v) // 42
}Buffered Channels
make(chan T, n) creates a buffered channel of capacity n. A send doesn't block until the buffer is full; a receive doesn't block while the buffer is non-empty. A buffer is not for "speeding things up" but for smoothing spikes or decoupling producer/consumer pacing.
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 // buffer full; the next send would block
fmt.Println(len(ch), cap(ch)) // 3 3
}Closing and range
close(ch) means "no more values will come". The receiver can use the second return value ok to distinguish "got a value" from "channel closed and empty"; range keeps reading until the channel is closed and drained.
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 3; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
// explicit check
v, ok := <-ch // closed and empty, ok=false
fmt.Println(v, ok)
}Directional Channels
Restricting a channel to send-only or receive-only in a function signature clarifies responsibility and prevents misuse at compile time. A bidirectional channel converts implicitly to a directional one, but not the other way around.
package main
import "fmt"
func producer(out chan<- int) {
for i := 0; i < 3; i++ {
out <- i
}
close(out)
}
func consumer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}select Multiplexing
select randomly picks one ready case to run; if none is ready it blocks, or runs default immediately if present. It's the core tool for timeouts, cancellation, and merging multiple sources.
package main
import (
"context"
"fmt"
"time"
)
func race(ctx context.Context) error {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
go func() { ch1 <- 1 }()
select {
case v := <-ch1:
fmt.Println("from ch1:", v)
case ch2 <- 100:
fmt.Println("sent to ch2")
case <-time.After(time.Second):
fmt.Println("timeout")
case <-ctx.Done():
return ctx.Err()
}
return nil
}
func main() {
_ = race(context.Background())
}The Clever Use of a nil Channel
Both sending to and receiving from a nil channel block forever, so inside a select you can set a case's channel to nil to "temporarily disable that branch". Often used to stop a case from firing repeatedly after a shutdown.
package main
import (
"context"
"fmt"
"time"
)
func run(ctx context.Context, resume <-chan struct{}) {
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
tickC := ticker.C // set to nil when you want to pause
paused := false
for {
select {
case <-tickC:
fmt.Println("tick")
if !paused {
paused = true
tickC = nil // mute this branch
}
case <-resume:
paused = false
tickC = ticker.C
case <-ctx.Done():
return
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
run(ctx, make(chan struct{}))
}worker pool (fan-out / fan-in)
Multiple goroutines consume the same jobs channel (fan-out) while results are aggregated into one results channel (fan-in) — Go's most classic concurrency model. Here is a complete runnable version:
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
time.Sleep(50 * time.Millisecond)
results <- j * j
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
var wg sync.WaitGroup
for w := 1; w <= 3; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
worker(id, jobs, results)
}(w)
}
// dispatch jobs
for j := 1; j <= 5; j++ { jobs <- j }
close(jobs)
// close results only after all workers exit
go func() { wg.Wait(); close(results) }()
for r := range results { fmt.Println(r) }
}done Channel vs context
Early Go used done := make(chan struct{}) + close(done) to broadcast cancellation; new code prefers context.Context — it unifies cancellation, timeout, and value passing in one standard interface and propagates automatically across RPC/HTTP.
package main
import (
"fmt"
"time"
)
func main() {
done := make(chan struct{})
for i := 0; i < 3; i++ {
i := i
go func() {
<-done
fmt.Println("bye", i)
}()
}
close(done) // one close; all waiters are notified at once
time.Sleep(100 * time.Millisecond)
}Common Pitfalls
- Sending/receiving on an uninitialized channel (var ch chan int): blocks forever
- Multiple goroutines write the same channel and all want to close it: wrap close with sync.Once
- For an unbounded queue: use a channel + slice buffer, or a slice + Mutex; don't grow the channel buffer without bound
- When several select cases are ready at once the choice is random; don't rely on order