Combine Or-Channel Patterns Like a Go Expert: Advanced Go Concurrency

Combine Or-Channel Patterns Like a Go Expert: Advanced Go Concurrency

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!

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.

要查看或添加评论,请登录

Archit Agarwal的更多文章

社区洞察

其他会员也浏览了