Working with Channels in Go
Channels are Go's built-in mechanism for communication and synchronization between goroutines. This guide covers everything you need to know about working with channels effectively.
Channel Basics
Channel Declaration
// Unbuffered channel
ch := make(chan int)
// Buffered channel with capacity 5
buffered := make(chan string, 5)
// Channel of channels
controlCh := make(chan chan bool)
// Receive-only channel
var readCh <-chan int
// Send-only channel
var writeCh chan<- int
Channel Operations
// Send value (blocks until received)
ch <- 42
// Receive value (blocks until sent)
value := <-ch
// Non-blocking send with select
select {
case ch <- value:
fmt.Println("Sent value")
default:
fmt.Println("Channel full")
}
// Non-blocking receive with select
select {
case value := <-ch:
fmt.Printf("Received: %v\n", value)
default:
fmt.Println("No value available")
}
Channel Closing
// Close channel
close(ch)
// Check if channel is closed
value, ok := <-ch
if !ok {
fmt.Println("Channel closed")
}
// Range over channel until closed
for value := range ch {
fmt.Printf("Received: %v\n", value)
}
Channel Types
Unbuffered Channels
// Synchronous communication
func ping(pinger chan<- string, msg string) {
pinger <- msg // Blocks until receiver is ready
}
func pong(pinger <-chan string, ponger chan<- string) {
msg := <-pinger // Blocks until sender sends
ponger <- msg + " pong"
}
// Usage
pinger := make(chan string)
ponger := make(chan string)
go ping(pinger, "ping")
go pong(pinger, ponger)
result := <-ponger
Buffered Channels
// Asynchronous communication
queue := make(chan string, 3)
// Non-blocking sends until buffer full
queue <- "first"
queue <- "second"
queue <- "third"
// Would block: queue <- "fourth"
// Receive values
fmt.Println(<-queue) // "first"
fmt.Println(<-queue) // "second"
fmt.Println(<-queue) // "third"
Directional Channels
// Channel direction
func send(ch chan<- int) {
ch <- 42 // Can only send
}
func receive(ch <-chan int) {
value := <-ch // Can only receive
}
// Bidirectional channel converted to directional
ch := make(chan int)
go send(ch) // Implicitly converted to send-only
go receive(ch) // Implicitly converted to receive-only
Channel Patterns
1. Generator Pattern
// Number generator
func generateNumbers(max int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; i < max; i++ {
ch <- i
}
}()
return ch
}
// Usage
for num := range generateNumbers(5) {
fmt.Printf("%d ", num)
}
2. Fan-out Pattern
// Distribute work across multiple workers
func fanOut(input <-chan int, workers int) []<-chan int {
outputs := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
outputs[i] = worker(input)
}
return outputs
}
func worker(input <-chan int) <-chan int {
output := make(chan int)
go func() {
defer close(output)
for n := range input {
output <- n * n
}
}()
return output
}
3. Fan-in Pattern
// Combine multiple inputs into single channel
func fanIn(inputs ...<-chan int) <-chan int {
output := make(chan int)
var wg sync.WaitGroup
// Start goroutine for each input channel
for _, ch := range inputs {
wg.Add(1)
go func(ch <-chan int) {
defer wg.Done()
for value := range ch {
output <- value
}
}(ch)
}
// Close output when all inputs are done
go func() {
wg.Wait()
close(output)
}()
return output
}
4. Pipeline Pattern
// Chain of processing stages
func pipeline(numbers <-chan int) <-chan int {
squared := square(numbers)
doubled := double(squared)
return doubled
}
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
}
func double(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * 2
}
}()
return out
}
Best Practices
1. Channel Ownership
// Good: Clear channel ownership
type Producer struct {
output chan int
}
func NewProducer() *Producer {
return &Producer{
output: make(chan int),
}
}
func (p *Producer) Start() {
go func() {
defer close(p.output)
// Send values
}()
}
func (p *Producer) Output() <-chan int {
return p.output // Return receive-only channel
}
2. Channel Closing
// Good: Single writer principle
func processData(input <-chan int) <-chan int {
output := make(chan int)
go func() {
defer close(output) // Close before returning
for value := range input {
output <- process(value)
}
}()
return output
}
// Bad: Multiple goroutines closing
func badPattern(ch chan int, count int) {
var wg sync.WaitGroup
for i := 0; i < count; i++ {
wg.Add(1)
go func() {
defer wg.Done()
defer close(ch) // Race condition!
// Process
}()
}
wg.Wait()
}
3. Error Handling
type Result struct {
Value int
Error error
}
func process(input <-chan int) <-chan Result {
output := make(chan Result)
go func() {
defer close(output)
for value := range input {
if value < 0 {
output <- Result{Error: fmt.Errorf("invalid value: %d", value)}
continue
}
output <- Result{Value: value * 2}
}
}()
return output
}
Common Patterns
1. Worker Pool
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
results <- job * 2
}
}
func WorkerPool(numWorkers int, jobs []int) []int {
jobsChan := make(chan int, len(jobs))
resultsChan := make(chan int, len(jobs))
// Start workers
for w := 1; w <= numWorkers; w++ {
go worker(w, jobsChan, resultsChan)
}
// Send jobs
for _, job := range jobs {
jobsChan <- job
}
close(jobsChan)
// Collect results
results := make([]int, len(jobs))
for i := range results {
results[i] = <-resultsChan
}
return results
}
2. Timeout Pattern
func timeoutPattern(input <-chan int, timeout time.Duration) (int, error) {
select {
case value := <-input:
return value, nil
case <-time.After(timeout):
return 0, fmt.Errorf("operation timed out")
}
}
// Usage
result, err := timeoutPattern(dataChan, 5*time.Second)
3. Rate Limiting
func rateLimiter(input <-chan int, rate time.Duration) <-chan int {
output := make(chan int)
ticker := time.NewTicker(rate)
go func() {
defer close(output)
for value := range input {
<-ticker.C // Wait for tick
output <- value
}
ticker.Stop()
}()
return output
}
// Usage
limited := rateLimiter(dataChan, 100*time.Millisecond)
Performance Considerations
1. Channel Size
// Choose buffer size based on expected load
const (
SmallBuffer = 10 // For low-volume communication
MediumBuffer = 100 // For medium-volume communication
LargeBuffer = 1000 // For high-volume communication
)
// Consider memory usage
type LargeData struct {
Data [1024]byte
}
ch := make(chan LargeData, LargeBuffer) // Uses significant memory
2. Channel vs Mutex
// Channel for communication
type Counter struct {
ch chan int
}
func (c *Counter) Increment() {
c.ch <- 1
}
// Mutex for simple state management
type CounterMutex struct {
mu sync.Mutex
count int
}
func (c *CounterMutex) Increment() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
3. Channel Cleanup
// Ensure channels are properly closed
func cleanup(chs ...*chan int) {
for _, ch := range chs {
if *ch != nil {
close(*ch)
*ch = nil
}
}
}
// Usage with defer
ch := make(chan int)
defer cleanup(&ch)
Next Steps
- Learn about goroutines
- Explore mutexes
- Study context
- Practice with select