Channels — Communication Between Goroutines
One of Go's famous design philosophy quotes:
"Do not communicate by sharing memory; instead, share memory by communicating."
Channels are the core tool that embodies this philosophy. They are typed pipes that safely pass values between goroutines.
Channel Basics — Creation and Usage
package main
import "fmt"
func main() {
// Create channel — make(chan type)
ch := make(chan int)
// Send value from goroutine
go func() {
ch <- 42 // Send value to channel
}()
// Receive value in main
value := <-ch // Receive value from channel
fmt.Println("Received:", value) // 42
}
Channel operations:
- Send:
ch <- value— blocks until receiver is ready - Receive:
value := <-ch— blocks until sender is ready
Unbuffered vs Buffered Channels
Unbuffered Channel
make(chan T) — send and receive must happen simultaneously. This is a ** rendezvous**mechanism.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string) // Unbuffered channel
go func() {
time.Sleep(1 * time.Second)
fmt.Println("Before sending")
ch <- "Hello" // Blocks until receiver is ready
fmt.Println("After sending")
}()
fmt.Println("Waiting for receive...")
msg := <-ch // Blocks until sender is ready
fmt.Println("Received:", msg)
}
// Output:
// Waiting for receive...
// Before sending
// After sending
// Received: Hello
Buffered Channel
make(chan T, capacity) — can send without blocking until the buffer is full.
package main
import "fmt"
func main() {
ch := make(chan int, 3) // Buffer size 3
// Send without blocking while buffer has space
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 // Buffer is full, would block!
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
// Channel length and capacity
ch2 := make(chan int, 5)
ch2 <- 10
ch2 <- 20
fmt.Printf("Length: %d, Capacity: %d\n", len(ch2), cap(ch2)) // Length: 2, Capacity: 5
}
| Type | Unbuffered | Buffered |
|---|---|---|
| Synchronization | Simultaneous send/receive | Asynchronous (within buffer) |
| Blocks when | No counterpart ready | Buffer full / empty |
| Use case | Synchronization signal | Buffering speed differences |
Directional Channels
Restricting channel direction in function parameters prevents mistakes.
package main
import "fmt"
// chan<- int : send-only channel
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
// <-chan int : receive-only channel
func consumer(ch <-chan int) {
for v := range ch {
fmt.Println("Received:", v)
}
}
func main() {
ch := make(chan int, 5) // Bidirectional channel
go producer(ch) // Auto-converts to send-only
consumer(ch) // Auto-converts to receive-only
}
Channel direction types:
chan T— bidirectional (read and write)chan<- T— send-only (write only)<-chan T— receive-only (read only)
Closing Channels
close(ch) closes a channel so no more values can be sent. Receivers can detect when a channel is closed.
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // Close the channel
// Method 1: Check channel state with ok pattern
for {
value, ok := <-ch
if !ok {
fmt.Println("Channel is closed")
break
}
fmt.Println("Value:", value)
}
// Method 2: range over channel (auto-exits when closed)
ch2 := make(chan string, 3)
ch2 <- "a"
ch2 <- "b"
ch2 <- "c"
close(ch2)
for s := range ch2 {
fmt.Println(s)
}
}
Channel closing rules:
- Sending to a closed channel → panic
- Receiving from a closed channel → zero value and false
- Closing an already closed channel → panic
- The sender should close the channel(receiver closing risks panicking the sender)
range-over-channel
Using range on a channel automatically receives until the channel is closed.
package main
import (
"fmt"
"sync"
)
func generateNumbers(start, end int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch) // Close channel when function exits
for i := start; i <= end; i++ {
ch <- i
}
}()
return ch
}
func main() {
// Pipeline pattern
numbers := generateNumbers(1, 10)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for n := range numbers { // Receives until channel closes
fmt.Printf("%d ", n)
}
fmt.Println()
}()
wg.Wait()
}
Pipeline Pattern
Connect channels to build a data processing pipeline.
package main
import "fmt"
// Stage 1: Generate numbers
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
// Stage 2: Square numbers
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
// Stage 3: Filter even numbers
func filterEven(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
if n%2 == 0 {
out <- n
}
}
}()
return out
}
func main() {
// Pipeline: generate → square → filterEven
nums := generate(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
squares := square(nums)
evens := filterEven(squares)
for v := range evens {
fmt.Printf("%d ", v) // 4 16 36 64 100
}
fmt.Println()
}
Fan-out, Fan-in Pattern
Distribute work to multiple goroutines (fan-out) and collect results (fan-in).
package main
import (
"fmt"
"sync"
)
// Fan-out: distribute one channel to multiple workers
func fanOut(input <-chan int, workers int) []<-chan int {
outputs := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
out := make(chan int)
outputs[i] = out
go func(out chan<- int) {
defer close(out)
for n := range input {
out <- n * n
}
}(out)
}
return outputs
}
// Fan-in: merge multiple channels into one
func fanIn(inputs ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
merged <- n
}
}
wg.Add(len(inputs))
for _, c := range inputs {
go output(c)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
func main() {
// Create input channel
input := make(chan int, 10)
for i := 1; i <= 10; i++ {
input <- i
}
close(input)
// Fan-out: distribute to 3 workers
workers := fanOut(input, 3)
// Fan-in: merge results
results := fanIn(workers...)
sum := 0
for r := range results {
sum += r
}
fmt.Println("Sum:", sum) // 385 (1²+2²+...+10²)
}
Key Takeaways
- Unbuffered channel: Synchronous communication, send and receive happen simultaneously
- Buffered channel: Asynchronous communication, no blocking within buffer size
- Directional channels: Use
chan<-,<-chanin function parameters to clarify intent - Closing channels: Sender closes; detect with
rangeorokpattern - Pipeline: Chain channels to compose data flows
- Fan-out/Fan-in: Parallel processing and result collection pattern