A channel provides a way for goroutines to communicate by passing messages. This approach follows the principle of sharing memory by communicating, distinct from the shared-memory model often used in other languages. The chan keyword declares a channel, specifying the type of data it carries.
var stream chan float64
At this point stream is nil and unusable until initialised with make.
Creating and Closing Channels
The make function creates a channel. It accepts the channel type and an optional capacity for buffering.
unbuffered := make(chan float64) // capacity 0
buffered := make(chan float64, 4) // capacity 4
A channel should be closed when no more values will be sent. The close built‑in performs this operation.
defer close(buffered)
Sending and Receiving
The arrow operator <- indicates direction: writing into a channel or reading from it.
package main
import "fmt"
func main() {
pipe := make(chan string, 1)
pipe <- "hello"
msg := <-pipe
fmt.Println(msg)
}
Channels behave like FIFO queues. Only one goroutine can send at a time, and only one can receive.
Unbuffered Channels
When the capacity is zero, no data can be stored in the channel. Sending blocks until another goroutine is ready to receive, and receiving blocks until a sender is ready. If the same goroutine tries to both send and receive synchronously on an unbuffered channel, a deadlock occurs.
// This deadlocks.
func main() {
ch := make(chan int)
ch <- 42
val := <-ch
fmt.Println(val)
}
To avoid deadlock, perform the operation in separate goroutines.
func main() {
ch := make(chan int)
go func() {
ch <- 42
}()
result := <-ch
fmt.Println(result)
}
The output is 42.
Buffered Channels
Adding a buffer allows a channel to hold a fixed number of values without an immediate receiver. A send blocks only when the buffer is full; a receive blocks only when the buffer is empty.
Basic Example
func main() {
queue := make(chan int, 2)
queue <- 10
queue <- 20
fmt.Println(<-queue) // 10
fmt.Println(<-queue) // 20
}
Producer‑Consumer with Buffer
In the following example, a producer fills the buffer, then waits while a consumer slowly empties it.
package main
import (
"fmt"
"time"
)
func main() {
pipe := make(chan int, 3)
doneWriter := make(chan bool)
doneReader := make(chan bool)
defer func() {
close(pipe)
}()
go func() {
for i := 1; i <= 8; i++ {
pipe <- i
fmt.Printf("sent %d\n", i)
}
doneWriter <- true
}()
go func() {
for i := 1; i <= 8; i++ {
time.Sleep(2 * time.Second)
val := <-pipe
fmt.Printf("received %d\n", val)
}
doneReader <- true
}()
<-doneWriter
<-doneReader
fmt.Println("all done")
}
Output illustrates the buffer filling, then alternating sends and receives as space becomes available.
Inspecting a Channel
len(ch) returns the current number of buffered elements. cap(ch) returns the total buffer capacity.
func main() {
ch := make(chan int, 5)
ch <- 7
ch <- 14
fmt.Printf("elements: %d, capacity: %d\n", len(ch), cap(ch))
}
// Output: elements: 2, capacity: 5
Using a Channel as a Mutex
A buffered channel of capacity 1 can serve as a mutual exclusion lock: acquiring the lock sends a token, releasing reads it back.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var counter int
lock := make(chan struct{}, 1)
wg.Add(2)
increment := func() {
lock <- struct{}{} // acquire
for i := 0; i < 1000; i++ {
counter++
}
<-lock // release
wg.Done()
}
decrement := func() {
lock <- struct{}{}
for i := 0; i < 500; i++ {
counter--
}
<-lock
wg.Done()
}
go increment()
go decrement()
wg.Wait()
fmt.Println(counter) // 500
}
The final value of counter is always 500, becuase the channel ensures exclsuive access.
Common Pitfalls
- Writing to or reading from a
nilchannel blocks forever. - Sending to a closed channel causes a panic.
- Closing a closed channel also panics.
- Closing a
nilchannel panics. - An unbuffered channel requires a concurrent sender and receiver; otherwise deadlock occurs.
- A buffered channel blocks on send when full and on receive when empty.