Go Channels
Channel Basics
Unbuffered channels — synchronous handoff
ch := make(chan string)
go func() {
ch <- "hello" // blocks until receiver is ready
}()
msg := <-ch // blocks until sender sends
fmt.Println(msg)
An unbuffered channel blocks the sender until a receiver is ready, and vice versa. Both goroutines meet at the send/receive.
Buffered channels — async with capacity
ch := make(chan int, 3) // buffer holds 3 values
ch <- 1 // does not block
ch <- 2 // does not block
ch <- 3 // does not block
// ch <- 4 // would block — buffer full
fmt.Println(<-ch) // 1 (FIFO)
Buffered channels decouple sender and receiver up to the buffer capacity.
Directional Channels
Restricting channel direction in function signatures
func produce(ch chan<- int) { // send-only
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
func consume(ch <-chan int) { // receive-only
for v := range ch {
fmt.Println(v)
}
}
Directional types are a compile-time constraint. A bidirectional chan int converts automatically.
Select
select multiplexes across channels
select {
case msg := <-ch1:
fmt.Println("from ch1:", msg)
case msg := <-ch2:
fmt.Println("from ch2:", msg)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
select blocks until one case is ready. If multiple are ready, one is chosen at random. time.After provides a built-in timeout.
Non-blocking with default
select {
case msg := <-ch:
fmt.Println("received:", msg)
default:
fmt.Println("no message available")
}
Closing Channels
close signals no more values
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
// range exits when channel is closed
for v := range ch {
fmt.Println(v)
}
// Manual close check
v, ok := <-ch
if !ok {
fmt.Println("channel closed")
}
Only the sender should close a channel. Sending on a closed channel panics. range over a channel is the idiomatic consumption pattern.
Patterns
Fan-in — merge multiple channels
func fanIn(channels ...<-chan string) <-chan string {
var wg sync.WaitGroup
merged := make(chan string)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan string) {
defer wg.Done()
for v := range c {
merged <- v
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
Done channel for cancellation
func doWork(done <-chan struct{}) {
for {
select {
case <-done:
return
default:
// work
}
}
}
done := make(chan struct{})
go doWork(done)
close(done) // broadcasts to all receivers
chan struct{} carries no data — it is a pure signal. Prefer context.Context in production code.