Refactor the API to use error channels where appropriate.
Previously, asynchronous functions in this interface would return channels of
"results" that can be either errors or values as shown below
type Result struct {
Value ValueType
Err error
}
func Request(context.Context) (results <-chan Result)
Unfortunately, there are several issues with this approach:
The basic problem with this approach is that it requires defining a new "result"
type and checking if each value is either a result or error. This is hardly a
show-stopper but it infects values with additional error fields.
However, the main issue with this approach is cancellation. If the context is
canceled, the Request implementation has a few options:
1. Close the results channel and walk away. Unfortunately, the caller will likely
interpret this as "request competed" instead of "request canceled". To correctly
use functions like this, the caller must check if the context after the channel
closes.
2. Leave the result channel open. In this case, the caller must select on its
own context. This is a pretty sane approach, but it makes it impossible to
safely use the results channel in a "for range" loop.
3. Write the cancellation error to the results channel. This will ensure that
the caller gets the error, but only if the caller is still reading from the
channel. If the caller isn't reading from the channel, the request's
goroutine will block trying to write to the channel and will never exit.
This patch solves this problem by returning an error channel with a buffer of 1
instead of a channel of results. This channel will yield at most one error
before it's closed. This pattern is used across a wide range of go projects
including Docker.
func Request(context.Context) (values <-chan ValueType, err <-chan error)
Unlike the previous API, this API is difficult to misuse.
1. Errors are clearly returned on the err channel so the user won't assume that
a closed value channel means the request succeeded.
2. The value channel is closed so "for range" loops work.
3. The error is always written to a channel with a buffer size of 1. If the
caller doesn't listen on this channel (e.g., because it doesn't care about the
error), nothing bad will happen.
The correct usage is:
values, err := Request(ctx)
for v := range values {
// do something
}
// wait for the request to yield the final error
// (or close the channel with no error).
return <-err
The main downside to this approach is that it requires multiple channels.