2

I am working on my first real Go project, a messaging API. I use channels to pass messages and other data between user goroutines and library goroutines that use a thread-unsafe, event-based C protocol library. For details https://github.com/apache/qpid-proton/blob/master/proton-c/bindings/go/README.md

My question is in 2 related parts:

1. What are common idioms for handling errors across channels?

The goroutine at one end blows up, how do I ensure the other end unblocks, gets an error value and doesn't get blocked again later?

For readers:

  • I can close the channel, but no error info.
  • I could pass a struct { data, error }
  • or use a second channel.

Pros & cons? Other ideas?

For writers: I can't close without a panic so I guess I need a second channel. Is this idiomatic?

select {
    case sendChan <- data: sentOk()
    case err := <- errChan: oops(err)
}

I also can't write after close so I need to store the error somewhere and check before trying to write. Any other approaches?

2. Exposing channels in APIs.

I need channels to pass error info: should I make those channels public fields or hide them in methods?

There is a tradeoff, and I don't have the experience to evaluate it:

  • Exposing channels lets users select directly, but it requires them to correctly impement the error handling patterns (check for errors before write, select for error as well as write). This seems complex and error-prone but maybe that because I'm not seasoned in go.

  • Hiding channels in a method simplifies and enforces correct use of the library. But now an async user must create their own goroutine and channel(s). They may just duplicate what the library does already, which is silly. Also there is an extra goroutine and channel on the path. Maybe that's not a big deal, but the data channel is the critical path for my library and I think it has to be hidden along with the error channel.

I could do both: expose the channels for power users and provide a simple method wrapper for people with simple needs. That's more to support but worth it if neither alone can fit all cases.

The standard net.Conn uses blocking methods, not channels, and I wrote goroutines to pump data to my C event-loop channel so I know it can be done, but I did not find it trivial. net.Conn is wrapping sytem calls not channels underneath so "exposing the channels" is not an option. Do any of the standard libraries export channels with error handling? (time.After doesn't count, there are no errors)

Thanks a lot! Alan

1 Answer 1

6

Your question is a bit on the broad side but I'll try to give some guidance based on my experience writing highly concurrent code...

Personally I think making the channel a property of the object that gets initialized in a nice helpful NewMyObject() *MyObject method is good design pattern. It makes it so code using the object doesn't have to do boiler plate set up every time it wants to call some asynchronous method the type offers.

For readers: I can close the channel, but no error info. I could pass a struct { data, error } or use a second channel. Pros & cons? Other ideas?

Let the reader signal to return by closing the abort channel. The reader should simply use the temp, err := <-FromChannel paradigm and move on with execution if the data or error channel has closed. This should prevent the 'send on closed channel' panics error from the workers since they will close their channel and return. When err != nil the reader will know to move on.

For writers: I can't close without a panic so I guess I need a second channel. Is this idiomatic?

Yes. Sadly I was quite pissed of with the uni-directional behavior of channels and though it should be abstracted. Regardless, it's not. In my code I would not define this on the object that does work asynchronously. The paradigm I prefer is to use the closing signal (since sending a on a channel is not one-to-many, only one goroutine will read that). Instead, I allocate the abort channel in the calling code and if things need to shut down you close the abort channel and all the goroutines doing asynchronous work who are listening on that channel do their clean up and return. You should also use a WaitGroup so you can wait for the goroutines to return before moving on.

So my basic summary;

1) let the caller of asynchronous methods signal it's time to stop, not the other way around. A waitgroup is better used to coordinate their returns

2) use a sync.WaitGroup in the calling code to know when your goroutines are finished so you can move on

3) allocate your error channel in the calling code and take advantage of the one-to-many signal produced by closing the channel; if you send on a channel you allocate in the caller, only a single instance will read from it. If you put one on each instance you have to iterate a collection of instances to send the on each.

4) if you have a type that provide async methods that do work in the background, set up the channels to read off of in it's initializer, document the async methods saying where to listen for data, provide an example of a non-blocking select that passes an abort channel into the async method and listens on the methods data and error channels. If you need to kill a single routine you could accomplish this by closing one of the channels it owns rather than killing them all by closing the callers abort channel.

Hopefully that all makes sense.

Sign up to request clarification or add additional context in comments.

4 Comments

That's really helpful. I like the abort channel being created by the caller. I did that already for message acknowledgements but somehow I didn't consider it for errors.
I don't follow this: "I would never have the worker close the channel it writes on" I thought it was only safe to close from the write end. Who should close the channel?
@AlanConway yeah, that doesn't make sense... I will edit it. The writer should close it's channel and return from the abort case.
it would be very useful to have some simple code example as addition for the answer.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.