Combine Or-Channel Patterns Like a Go Expert: Advanced Go Concurrency
Archit Agarwal
PMTS @ Oracle | Golang | Docker | Kubernetes| Typescript | Node.js | .NET | Angular | AWS Certified Cloud Practitioner | Educator | 2 Lac world wide rank on Leetcode
Have you ever faced a situation where one goroutine depends on data from multiple others? Imagine you're querying multiple servers for the same data. If any server responds successfully, you can cancel the remaining requests. It's a classic case where combining multiple "done" channels becomes essential.
This article explores how to handle such scenarios in Go by implementing an or function—a concurrency pattern that merges multiple done channels. Let’s dive in and learn how to simplify complex channel orchestration!
The Problem: Combining Multiple Done Channels
Here’s the scenario: you have several goroutines, each working independently, but your program only cares when any one of them finishes. Think of it like an "or" condition for your channels.
But wait, what if you don’t even know how many such channels you have in advance? Sounds tricky, right? Fear not, because we’re about to tackle this elegantly with recursion.
Building the or Function
The or function will take multiple <- chan interface{} channels and return a single <- chan interface{} that signals when any of the input channels signal completion.
Here’s the function signature:
var or func(doneChannels ...<-chan interface{}) <-chan interface{}
Case 1: One or Fewer Channels
Let’s start simple. If the function receives one or no channels:
or = func(doneChannels ...<-chan interface{}) <-chan interface{} {
switch len(doneChannels) {
case 0:
return nil
case 1:
return doneChannels[0]
}
return nil
}
Case 2: Two Channels
What if we have two channels? We use a select statement to wait on either channel:
领英推荐
or = func(doneChannels ...<-chan interface{}) <-chan interface{} {
switch len(doneChannels) {
case 0:
return nil
case 1:
return doneChannels[0]
}
done := make(chan interface{})
go func() {
defer close(done)
select {
case <-doneChannels[0]:
case <-doneChannels[1]:
}
}()
return done
}
Case 3: Many Channels
Handling three or more channels with nested select blocks quickly becomes unmanageable. Instead, we turn to recursion to handle any number of channels:
or = func(doneChannels ...<-chan interface{}) <-chan interface{} {
switch len(doneChannels) {
case 0:
return nil
case 1:
return doneChannels[0]
}
done := make(chan interface{})
go func() {
defer close(done)
select {
case <-doneChannels[0]:
case <-doneChannels[1]:
case <-or(doneChannels[2:]...): // Recursive call for remaining channels
}
}()
return done
}
This recursive approach scales beautifully, allowing the or function to merge any number of channels without unnecessary complexity.
Putting It All Together
Here’s a practical example of using the or function:
sig := func(after time.Duration) <-chan interface{} {
c := make(chan interface{})
go func() {
defer close(c)
time.Sleep(after)
}()
return c
}
start := time.Now()
<-or(
sig(2*time.Hour),
sig(5*time.Minute),
sig(1*time.Second),
sig(1*time.Hour),
sig(1*time.Minute),
)
fmt.Printf("Done after %v\n", time.Since(start))
Output:
Done after 1.002543s
Even though some channels would take hours to complete, the parent goroutine finishes as soon as the fastest channel (1 second) signals completion.
Wrapping It Up
Combining multiple done channels with an or pattern is a powerful way to simplify your Go concurrency code. By using recursion, you avoid hardcoding limits and handle dynamic scenarios with ease.
This pattern is not just elegant but also practical for real-world applications like managing multiple timeouts, aggregating results, or building resilient systems.
If you found this example helpful, make sure to share it with your peers who are exploring Go concurrency. For more advanced patterns, subscribe to The Weekly Golang Journal and never miss an update! Let’s keep levelling up our Go skills together. Do you have questions or ideas? Drop them in the comments below—I’d love to hear from you!
IT-Consultant
1 个月The examples you are providing can be simplified by just using e.g. a WaitGroup with count 1. The first function completing successfully can call wg.Done() and in the method you want to wait you can call wg.Wait(). No need for recursion or overusing channels.