go-channel-patterns
Common channel patterns and idioms
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.
| name | go-channel-patterns |
|---|---|
| description | Common 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
- Use directional channels:
<-chan(receive),chan<-(send) - Unbuffered for synchronization, buffered for non-blocking
- Check
okwhen not usingrange:v, ok := <-ch - Close to signal "no more values", not to stop receivers
- 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)