4
\$\begingroup\$

I have a simple golang app in which I have two background tasks that produce data,each of them use it's own channel to signal it produced another unit of data.

I have a single consumer that needs the data form the two other background tasks as input in a 1-to-1 ratio. What I want to do,for the lack of a better term, is "Intersecting" the channels. Meaning I want to create a new channel that gets a signal every time there's a signal in one channel and then the other.

it's also worth mentioning I have a context I use to close all background gorotines to gracefully shutdown.

here is the function I wrote to do it:

func intersectChans(a <-chan bool, b <-chan bool) {
    for {
        select {
        case <-a:
            select {
            case <-b:
                c <- true
            case <-ctx.Done():
                return
            }
        case <-ctx.Done():
            return
        }
    }
}

I wonder if there is a more elegant/idiomatic way to achieve this

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

"Actually you code is pretty much this. Simple and boring as most other good Go code" I thought.

At this point I could say "Wrap your c <- true with a select statement, add the third case <-ctx.Done: to handle possible deadlock and you are ready to go".

But.

Does it really should be this way? Yep, this works and does the job, but are there any pitfalls we missed? Let see.

Indentation

Let's talk about idiomatic Go. Within the space of idiomatic Go a proper indentation is a single Tab rendered as 8 spaces:

func intersectChans(a <-chan bool, b <-chan bool) {
        for {
                select {
                case <-a:
                        select {
                        case <-b:
                                select {
                                case c <- true:
                                case <-ctx.Done():
                                        return
                                }
                        case <-ctx.Done():
                                return
                        }
                case <-ctx.Done():
                        return
                }
        }
}

Okay, the first pitfall we have is a deep code indentation.

Well, not really a problem because, khm, Idiomatic Go, it is you, who decided to render 8 spaces for a single Tab in the first place. And the Linux kernel C style guide as well.

Actually there is a good reason for this. Let me quote the style guide I linked above:

Now, some people will claim that having 8-character indentations makes the code move too far to the right, and makes it hard to read on a 80-character terminal screen. The answer to that is that if you need more than 3 levels of indentation, you’re screwed anyway, and should fix your program.

OK, we definitely have "too far to the right" movement here. Let's consider this a pitfall, but not a critical one.

Channels

This code is all about the channels. We have 4 here:

  • a and b channels to receive data
  • c channel to send aggregated results from a and b
  • ctx.Done() channel to terminate the goroutine

What can we do with channels? We can:

  • receive
  • send
  • and close the channel

ctx.Done() channel is closed to tell goroutine that the task is done. But what about our own channels?

You needn't close every channel when you've finished with it. It's only necessary to close a channel when it is important to tell the receiving goroutines that all data have been sent. A channel that the garbage collector determines to be unreachable will have its resources reclaimed whether or not it is closed.

says some book about Go.

OK, so we can close a channel to notify its reader, that all data has been sent.

But a read from a closed channel still returns some data. It will be a zero value of the channel data type. To actually check, that the channel was closed, we can use the "comma ok" idiom:

data, ok := <- a
if !ok {
        fmt.Println("a was closed")
}

OK, but why will we do it in the first place?

A not so boring solution

If we combine our strive to have a more flat code with an ability to check for closed channels we can come up with a solution like this:

func join(a, b <-chan int, r chan<- []int) {
        defer close(r)
        for {
                v1, ok1 := <-a
                v2, ok2 := <-b
                if !(ok1 && ok2) {
                        return
                }

                r <- []int{v1, v2}
        }
}

In this code sample we are using "comma ok" idiom to test for a closed worker channels and stop processing.

This code relies on the fact, that worker goroutines will close their channels to notify us, that all data has been sent. We do the same and close our channel with a defer statement.

And here are the missing parts (also on Go Playground):

package main

import (
        "context"
        "fmt"
        "time"
)

func worker(ctx context.Context, c chan<- int) {
        defer close(c)
        for i := 0; ; i++ {
                select {
                case <-ctx.Done():
                        return
                case c <- i:
                }
                time.Sleep(time.Second / 4)
        }
}

func join(a, b <-chan int, r chan<- []int) {
        defer close(r)
        for {
                v1, ok1 := <-a
                v2, ok2 := <-b
                if !(ok1 && ok2) {
                        return
                }

                r <- []int{v1, v2}
        }
}

func main() {
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()

        a := make(chan int)
        b := make(chan int)
        r := make(chan []int)

        go worker(ctx, a)
        go worker(ctx, b)
        go join(a, b, r)

        for pair := range r {
                fmt.Printf("got %+v\n", pair)
        }
}

"So, which solution should I take?" will you ask.

It depends. One solution is a simple and boring Go code. The other is "not that far to the right", but has some fancy channel operations not everybody even know exists.

And while you decide, don't forget to fix a possible deadlock in your actual code :)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.