On this page
Channels and Select
Channels: Safe Communication Between Goroutines
Go's philosophy on concurrency is summarized in a phrase by Rob Pike: "Do not communicate by sharing memory; instead, share memory by communicating." Channels are the mechanism to implement this philosophy.
A channel is a typed conduit through which goroutines send and receive values. They are fundamental to Go concurrency: instead of using mutexes to protect shared data, you pass data through channels, eliminating the possibility of concurrent access to the same variable.
Creating and Using Channels
// Create an unbuffered channel for int
ch := make(chan int)
// Create a buffered channel with 5 slots
ch2 := make(chan string, 5)
// Send a value (arrow operator <-)
ch2 <- "hello"
// Receive a value
msg := <-ch2
// Receive and discard the value (synchronization only)
<-ch2Unbuffered Channels
An unbuffered channel is synchronous: the send blocks until there is a receiver, and the receiver blocks until there is a sender. This synchronization guarantees that both goroutines "meet" at the channel point:
func main() {
ch := make(chan string)
go func() {
fmt.Println("Goroutine: preparing message...")
ch <- "Hello from the goroutine!" // blocks until main receives
fmt.Println("Goroutine: message sent")
}()
fmt.Println("Main: waiting for message...")
msg := <-ch // blocks until the goroutine sends
fmt.Println("Main received:", msg)
}
// Output (deterministic):
// Main: waiting for message...
// Goroutine: preparing message...
// Goroutine: message sent
// Main received: Hello from the goroutine!Unbuffered channels are used for synchronization more than for data transfer.
Buffered Channels
A buffered channel is asynchronous until the buffer is full. The send only blocks when the buffer is full, and the receive only blocks when the buffer is empty:
ch := make(chan int, 3) // buffer of 3 slots
ch <- 1 // does not block — buffer[0]
ch <- 2 // does not block — buffer[1]
ch <- 3 // does not block — buffer[2]
// ch <- 4 // would block! buffer full
fmt.Println(len(ch)) // 3 — elements in buffer
fmt.Println(cap(ch)) // 3 — buffer capacity
n := <-ch // 1 — does not block, buffer has elements
fmt.Println(n)
ch <- 4 // now fits — buffer: [2, 3, 4]Directional Channels
When passing a channel as a parameter, you can specify whether it can only be used for sending or receiving. This documents the intent and the compiler verifies the usage:
// Send-only (producer)
func producer(ch chan<- int) {
for i := 1; i <= 10; i++ {
ch <- i
}
close(ch)
// val := <-ch // compile error: cannot receive from send-only channel
}
// Receive-only (consumer)
func consumer(ch <-chan int) {
for n := range ch {
fmt.Println(n)
}
// ch <- 42 // compile error: cannot send to receive-only channel
}
func main() {
ch := make(chan int, 5) // chan int (bidirectional)
go producer(ch) // implicitly converted to chan<- int
consumer(ch) // implicitly converted to <-chan int
}Closing Channels and range
Closing a channel signals that no more values will be sent. Receivers can detect this:
// Detect closed channel with "comma ok"
for {
val, ok := <-ch
if !ok {
fmt.Println("Channel closed")
break
}
fmt.Println(val)
}
// Idiomatic form: range stops automatically when the channel is closed
for val := range ch {
fmt.Println(val)
}
// The loop ends when ch is closed and drainedImportant rule: Only the producer should close the channel. Closing from the consumer side causes bugs. Sending to a closed channel causes a panic.
The `select` Statement
select allows a goroutine to wait on multiple channel operations simultaneously. It is like a switch but for channel operations:
select {
case msg1 := <-ch1:
fmt.Println("Received from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Received from ch2:", msg2)
case ch3 <- "sent":
fmt.Println("Sent to ch3")
}If multiple cases are ready simultaneously, select picks one at random.
`select` with `default`
The default case makes select non-blocking:
// Non-blocking receive
select {
case val := <-ch:
fmt.Println("Received:", val)
default:
fmt.Println("Channel empty — no blocking")
}
// Non-blocking send
select {
case ch <- value:
fmt.Println("Sent")
default:
fmt.Println("Channel full — no blocking, value discarded")
}`select` in a Loop — Handling Multiple Sources
func multiplex(ch1, ch2 <-chan string, done <-chan struct{}) {
for {
select {
case msg := <-ch1:
processMessage("source1", msg)
case msg := <-ch2:
processMessage("source2", msg)
case <-done:
fmt.Println("Stop signal received")
return
}
}
}Timeouts with `time.After`
time.After(d) returns a channel that receives a value after duration d. It is perfect for implementing timeouts on channel operations:
func searchWithTimeout(id int, timeout time.Duration) (*Result, error) {
resultCh := make(chan *Result, 1)
go func() {
// potentially slow operation
result := searchDatabase(id)
resultCh <- result
}()
select {
case result := <-resultCh:
return result, nil
case <-time.After(timeout):
return nil, fmt.Errorf("timeout after %v searching id=%d", timeout, id)
}
}
// Usage
result, err := searchWithTimeout(42, 5*time.Second)Done Channel as Termination Signal
An empty chan struct{} is used to signal termination without transferring data:
func worker(jobs <-chan Job, done <-chan struct{}) {
for {
select {
case job := <-jobs:
processJob(job)
case <-done:
fmt.Println("Worker stopped")
return
}
}
}
func main() {
jobs := make(chan Job, 10)
done := make(chan struct{})
go worker(jobs, done)
// ... send jobs ...
// Signal stop
close(done) // closing a channel broadcasts to all receivers
}Pattern: Pipeline with Channels
A pipeline chains processing stages, each in its own goroutine:
// Stage 1: generate numbers
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// Stage 2: squares
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
// Chain the pipeline
naturals := generate(1, 2, 3, 4, 5)
squares := square(naturals)
for n := range squares {
fmt.Println(n) // 1, 4, 9, 16, 25
}
}With channels and select mastered, in the next lesson we will explore more advanced concurrency patterns: worker pools, fan-in/fan-out, and the essential context.Context.
Sign in to track your progress