1. go
  2. /concurrency
  3. /channels

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

  1. Learn about goroutines
  2. Explore mutexes
  3. Study context
  4. Practice with select

Additional Resources