go-channel-patterns

JamesPrial's avatarfrom JamesPrial

Common channel patterns and idioms

⭐2stars🔀0forks📁View on GitHub🕐Updated Dec 28, 2025⬇️1downloads

When & Why to Use This Skill

This Claude skill provides a comprehensive reference for Go (Golang) channel patterns and concurrency idioms. It focuses on best practices for safe communication between goroutines, including the fundamental 'sender closes' rule and advanced structural patterns like Fan-Out, Fan-In, and Or-Done. By implementing these standardized patterns, developers can build high-performance, leak-free, and maintainable concurrent systems while avoiding common pitfalls like deadlocks and panics.

Use Cases

  • Designing high-throughput data processing pipelines using Fan-In and Fan-Out patterns to distribute and aggregate workloads.
  • Implementing robust resource management and early exit strategies using the Or-Done pattern to prevent goroutine leaks.
  • Refactoring legacy Go codebases to adhere to idiomatic concurrency standards and improve system stability.
  • Establishing architectural guidelines for synchronization in microservices to ensure thread-safe data handling.
  • Optimizing application performance by correctly choosing between buffered and unbuffered channels for synchronous or asynchronous communication.
namego-channel-patterns
descriptionCommon channel patterns and idioms

Channel Patterns

Core Rule

Only the sender closes channels. Receivers never close.

CORRECT - Sender closes

func generator() <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch) // Sender closes
        for i := 0; i < 10; i++ {
            ch <- i
        }
    }()
    return ch
}

func main() {
    for num := range generator() {
        fmt.Println(num)
    }
}

WRONG - Receiver closes

func main() {
    ch := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            ch <- i // Panic: send on closed channel
        }
    }()

    for i := 0; i < 5; i++ {
        <-ch
    }
    close(ch) // WRONG: receiver closes
}

Pattern: Fan-Out (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 {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range input {
            out <- n * 2
        }
    }()
    return out
}

Pattern: Fan-In (Merge Channels)

func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup

    wg.Add(len(channels))
    for _, ch := range channels {
        go func(c <-chan int) {
            defer wg.Done()
            for n := range c {
                out <- n
            }
        }(ch)
    }

    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

Pattern: Or-Done (Early Exit)

func orDone(ctx context.Context, ch <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for {
            select {
            case <-ctx.Done():
                return
            case v, ok := <-ch:
                if !ok {
                    return
                }
                select {
                case out <- v:
                case <-ctx.Done():
                    return
                }
            }
        }
    }()
    return out
}

Rules

  1. Use directional channels: <-chan (receive), chan<- (send)
  2. Unbuffered for synchronization, buffered for non-blocking
  3. Check ok when not using range: v, ok := <-ch
  4. Close to signal "no more values", not to stop receivers
  5. Nil channels block forever (useful in select)

Buffered vs Unbuffered

// Unbuffered: synchronous handoff
ch := make(chan int)

// Buffered: decouples sender/receiver
ch := make(chan int, 10)