Channels
Channels are typed conduits for sending values between goroutines. Go's mantra is: "Do not communicate by sharing memory; share memory by communicating." Channels are the mechanism that makes this possible.
Creating and Using Channels
Use make(chan T) to create an unbuffered channel:
package main
import "fmt"
func sum(nums []int, ch chan int) {
total := 0
for _, n := range nums {
total += n
}
ch <- total // send total to channel
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
ch := make(chan int)
go sum(nums[:5], ch)
go sum(nums[5:], ch)
a, b := <-ch, <-ch // receive two values
fmt.Println(a + b) // 55
}An unbuffered channel synchronises sender and receiver — the send blocks until a receiver is ready, and vice versa.
Buffered Channels
A buffered channel has an internal queue. Sends only block when the buffer is full; receives only block when the buffer is empty:
package main
import "fmt"
func main() {
ch := make(chan string, 3)
ch <- "first"
ch <- "second"
ch <- "third"
// ch <- "fourth" // would block — buffer full
fmt.Println(<-ch) // first
fmt.Println(<-ch) // second
fmt.Println(<-ch) // third
}Idiomatic Go: Prefer unbuffered channels for synchronisation and buffered channels only when you know the exact capacity needed (e.g., a semaphore).
Ranging over Channels and Closing
Use range to receive all values until the channel is closed:
package main
import "fmt"
func generate(n int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; i < n; i++ {
ch <- i * i
}
}()
return ch
}
func main() {
for v := range generate(6) {
fmt.Print(v, " ") // 0 1 4 9 16 25
}
fmt.Println()
}Only the sender should close a channel. Sending to a closed channel panics.
Directional Channels
Functions can restrict channels to send-only (chan<- T) or receive-only (<-chan T):
package main
import "fmt"
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
func consumer(ch <-chan int) {
for v := range ch {
fmt.Println("received:", v)
}
}
func main() {
ch := make(chan int, 5)
go producer(ch)
consumer(ch)
}Directional channel types make the data flow explicit and prevent accidental misuse.
select
select waits on multiple channel operations and executes the first ready case. If multiple cases are ready, one is chosen at random:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Millisecond)
ch1 <- "one"
}()
go func() {
time.Sleep(2 * time.Millisecond)
ch2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("received from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("received from ch2:", msg2)
}
}
}Non-blocking Operations with default
Adding a default case makes select non-blocking:
package main
import "fmt"
func main() {
ch := make(chan int, 1)
select {
case v := <-ch:
fmt.Println("received:", v)
default:
fmt.Println("no value ready") // runs because ch is empty
}
ch <- 42
select {
case v := <-ch:
fmt.Println("received:", v) // 42
default:
fmt.Println("no value ready")
}
}Timeout Pattern
Use time.After in a select to implement timeouts:
package main
import (
"fmt"
"time"
)
func slowOperation(ch chan<- string) {
time.Sleep(200 * time.Millisecond)
ch <- "result"
}
func main() {
ch := make(chan string, 1)
go slowOperation(ch)
select {
case result := <-ch:
fmt.Println("got:", result)
case <-time.After(100 * time.Millisecond):
fmt.Println("timed out")
}
}Key Takeaways
- Channels are typed, goroutine-safe queues; unbuffered channels synchronise sender and receiver.
- Buffered channels decouple producer and consumer up to the buffer capacity.
- Only the sender should close a channel; use
rangeto receive until closed. - Directional channel types (
chan<-,<-chan) make data flow explicit. selectmultiplexes channel operations;defaultmakes it non-blocking.