Channels are typed conduits that allow goroutines to communicate by sending and receiving values, acting as the primary synchronization mechanism in Go. They work by blocking the sender until a receiver is ready and blocking the receiver until a value is available, ensuring safe data exchange without explicit locks.
You create a channel using make, specifying the data type. By default, channels are unbuffered, meaning a send operation blocks until another goroutine receives the value. Buffered channels allow a specific number of values to be stored without a receiver, preventing the sender from blocking until the buffer is full.
Here is a practical example of an unbuffered channel ensuring strict synchronization between a producer and a consumer:
package main
import (
"fmt"
"time"
)
func main() {
// Create an unbuffered channel for integers
ch := make(chan int)
// Start a goroutine to send data
go func() {
for i := 1; i <= 3; i++ {
fmt.Printf("Sending %d\n", i)
ch <- i // Blocks until main() receives
time.Sleep(100 * time.Millisecond)
}
close(ch) // Signal no more data will be sent
}()
// Receive data in the main goroutine
for val := range ch {
fmt.Printf("Received %d\n", val)
}
}
In this scenario, the ch <- i line pauses the goroutine until the range loop in main executes val := <-ch. This prevents race conditions because the data transfer is atomic and synchronized by the channel itself.
For scenarios where you don't want the sender to wait, use a buffered channel. This is useful for decoupling producer and consumer speeds:
package main
import (
"fmt"
)
func main() {
// Create a buffered channel with capacity of 2
ch := make(chan int, 2)
// These sends will NOT block because the buffer has space
ch <- 1
ch <- 2
// This send WILL block because the buffer is full
// Uncommenting the line below would cause a deadlock if no receiver exists
// ch <- 3
fmt.Println("Sent 1 and 2 successfully")
// Drain the buffer
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Key behaviors to remember:
- Blocking: Unbuffered channels synchronize execution; buffered channels allow limited asynchrony.
- Closing: Always close channels from the sender side to signal completion. Receiving from a closed channel returns the zero value of the type and
falsefor the second return value (e.g.,val, ok := <-ch). - Deadlocks: If you send to a full buffered channel or an unbuffered channel with no receiver, the program panics with a deadlock error.
Channels are the "pipes" of Go concurrency. Use them to pass data between goroutines rather than sharing memory, adhering to the rule: "Do not communicate by sharing memory; instead, share memory by communicating."