Asynchronous error handling in Go

3,646 views
Skip to first unread message

aco...@redhat.com

unread,
May 7, 2015, 3:30:50 PM5/7/15
to golan...@googlegroups.com
My first real Go project (a messaging library) uses channels to pass data between arbitrary user goroutines and internal goroutines that use a single-threaded C library (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 unblock the other end, give it an `error` and make sure it doesn't block again later?

For receivers I can close the data channel. For the `error` I could pass `struct { data, error }`, or set a struct `error` field, or use a second `chan error`. Pros & cons? Other ideas?

For senders I can't close without a panic. Is this the right way to handle it?

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

Send on a closed channel also panics so I need an `error` field as well, so code can check for error before attempting to write.

Is there a common pattern for how to organize all this? Other approaches I've missed?

2. Exposing channels in APIs.

Should the channels be exported or hidden in methods? There is a tradeoff, and I don't have the experience to evaluate it:

Exposing channels lets users select directly, but they must implement the error handling patterns of 1. correctly which seems complex and error-prone.

I can hide the channels in blocking methods that return `error`. This is simpler for simple use, but an async user has to wrap it in their own goroutine loops and error channels, which just duplicates what is already in the library! That seems a bit silly and adds overhead. The overhead may be small but this is my critical path.

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 can fit all cases well.

The standard net.Conn uses blocking methods, and I wrote goroutines with data error channels to pump data to my internal goroutines. It can be done, but I did not find it trivial. I would have liked net.Conn to provide a `chan []buf` interface for me. On the other hand I'd hate if net.Conn *only* had a channel interface and no simple Read/Write interface. So maybe both is the answer.

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


Giulio Iotti

unread,
May 11, 2015, 6:42:30 AM5/11/15
to golan...@googlegroups.com
On Thursday, May 7, 2015 at 9:30:50 PM UTC+2, aco...@redhat.com wrote:
My first real Go project (a messaging library) uses channels to pass data between arbitrary user goroutines and internal goroutines that use a single-threaded C library (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 unblock the other end, give it an `error` and make sure it doesn't block again later?

For receivers I can close the data channel. For the `error` I could pass `struct { data, error }`, or set a struct `error` field, or use a second `chan error`. Pros & cons? Other ideas?

I think it makes more sense to pack together the data and error in one channel. It's a lot like a normal return from a function. This way you can (1) return data and error at the same time (for example, last data and io.EOF); (2) Multiple error sources don't end up on the same channel, making it easy to understand what routine sent the error.
 

For senders I can't close without a panic. Is this the right way to handle it?

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

I am not sure I understand, but the usual idiom is to use a channel that is closed on error by the receiver (close is broadcasted to all listeners) to make all the senders exit.
 
Send on a closed channel also panics so I need an `error` field as well, so code can check for error before attempting to write.

Is there a common pattern for how to organize all this? Other approaches I've missed?

 

2. Exposing channels in APIs.

Should the channels be exported or hidden in methods?

It's generally better not to expose channels and other internals. For one thing, it's not so easy to document when you have to allocate a channel, close it, etc. 

You might also end up checking if the user channel is not nil a lot, which is not nice. Better to encapsulate all uses of the channel in the API.

If you think that only exporting a blocking API makes your program less concurrent, probably you need to rethink how the concurrency works.

-- 
Giulio Iotti

aco...@redhat.com

unread,
May 12, 2015, 5:34:06 PM5/12/15
to golan...@googlegroups.com


On Monday, May 11, 2015 at 6:42:30 AM UTC-4, Giulio Iotti wrote:
[snip] thanks for the tips! One more question.



2. Exposing channels in APIs.

Should the channels be exported or hidden in methods?

It's generally better not to expose channels and other internals. For one thing, it's not so easy to document when you have to allocate a channel, close it, etc. 

You might also end up checking if the user channel is not nil a lot, which is not nice. Better to encapsulate all uses of the channel in the API.

If you think that only exporting a blocking API makes your program less concurrent, probably you need to rethink how the concurrency works.

I would prefer to encapsulate. My concern is not concurrency but overhead. For example if I have an API function:

func (s Sender) Send(m Message) error {
   
select {
        s
.messageChan<-m: return nil // send the message
       
<-s.errorChan: return s.Error() // close means error
   
}
}

For simple use this is perfect. Howver a user that wants to pipeline messages into Send will have a goroutine reading from a channel and calling Send in a loop  They will need their own error back-channel to stop their upstream if Send() returns an error. This is duplicating exactly what Send is already doing and adding unnecessary overhead of another goroutine and channel. Maybe it doesn't matter, I'm not experienced with the costs of channels and goroutines. I know they are cheap, but nothing is free and this is in the innermost loop that will determine the performance of the library.






Giulio Iotti

unread,
May 13, 2015, 2:21:38 AM5/13/15
to golan...@googlegroups.com
On Wednesday, May 13, 2015 at 12:34:06 AM UTC+3, aco...@redhat.com wrote:
I would prefer to encapsulate. My concern is not concurrency but overhead. For example if I have an API function:

func (s Sender) Send(m Message) error {
   
select {
        s
.messageChan<-m: return nil // send the message
       
<-s.errorChan: return s.Error() // close means error
   
}
}

For simple use this is perfect. Howver a user that wants to pipeline messages into Send will have a goroutine reading from a channel and calling Send in a loop  They will need their own error back-channel to stop their upstream if Send() returns an error. This is duplicating exactly what Send is already doing and adding unnecessary overhead of another goroutine and channel. Maybe it doesn't matter, I'm not experienced with the costs of channels and goroutines. I know they are cheap, but nothing is free and this is in the innermost loop that will determine the performance of the library.

Once more, I talk without really having looked at the code. But your example Send is problematic:

1. A first user go routine sends to messageChan, m is processed without errors;
2. Another user go routine sends on messageChan (calls Send), this time it gets an error;
3. What assures you that the error will be read by the second go routine (in Send) and not the first?

I think you should use a more "request/response" approach, without sharing errorChan. You can create a channel for each request, the overhead is really a minor concern (and at least the code works as intended.)

aco...@redhat.com

unread,
May 14, 2015, 8:33:30 PM5/14/15
to golan...@googlegroups.com


On Wednesday, May 13, 2015 at 2:21:38 AM UTC-4, Giulio Iotti wrote:
On Wednesday, May 13, 2015 at 12:34:06 AM UTC+3, aco...@redhat.com wrote:
I would prefer to encapsulate. My concern is not concurrency but overhead. For example if I have an API function:

func (s Sender) Send(m Message) error {
   
select {
        s
.messageChan<-m: return nil // send the message
       
<-s.errorChan: return s.Error() // close means error
   
}
}
 
Once more, I talk without really having looked at the code. But your example Send is problematic:

1. A first user go routine sends to messageChan, m is processed without errors;
2. Another user go routine sends on messageChan (calls Send), this time it gets an error;
3. What assures you that the error will be read by the second go routine (in Send) and not the first?

I think you should use a more "request/response" approach, without sharing errorChan. You can create a channel for each request, the overhead is really a minor concern (and at least the code works as intended.)

 
You are quite right, my example is incomplete. I am concerned about two types of outcome. To send a message you need a live "sender" object. There are a bunch of protocol events that can close the sender. Once closed it stays closed so it's OK to return the same error. That's what I will use the above pattern for (the pattern occurs in other languages, though implemented a little differently.)

There is another class of outcome, an "acknowledgement" which applies to an individual message: was it accepted, rejected etc. My real API *also* returns a new channel from Send() for the acknowledgement. I think this is what you mean by request/response. I need both.

Thanks a lot for the tips, I think I know how to "do it right" now.
Reply all
Reply to author
Forward
0 new messages