An (Idiomatic?) Go Future Implementation
The future pattern is almost ubiquitous in software development, but you don't often see it in Go. I think there are a number of good reasons for this – one being the lack of support in the standard library or any good community supported packages.
Flawed Approaches
Most articles and packages dealing with futures in Go treat chan interface{}
as a future. While this seems like the idiomatic approach, it has a couple of major flaws.
- It requires the use of
interface{}
or code generation to write a general purpose implementation. - A value can only be received from
chan interface{}
once. Extra code must be written if the future needs to be used by multiple downstream actors. - It is easy to accidentally receive the zero value after the channel has been closed.
An (Idiomatic?) Implementation
Taking inspiration from the context package, closing a chan struct{}
can be used to signal when a Future
has resolved.
// Future is a synchronization primitive that guards data that may be
// available in the future.
type Future interface {
// Done returns a channel that is closed when the Future is
// resolved. It is safe to call Done multiple times across
// multiple threads.
Done() <-chan struct{}
}
This approach differs from most future implementation by not acting as a container for data, but as a guard for data that will be available in the future.
// Define a value that will be available in the future.
var val string
fut, resolve := async.NewFuture()
// Simulate long computation or IO by sleeping before setting the value and resolving
// the future.
go func() {
time.Sleep(500 * time.Millisecond)
val = "Hello World!"
resolve()
}()
// Block until the future is resolved.
<- fut.Done()
fmt.Println(val)
In practice a Future
can be embedded into a container to more closely match the traditional behavior of a future.
type Container struct {
async.Future
Value interface{}
Err error
}
func NewContainer() (*Container, func(interface{}, error)) {
fut, resolve := async.NewFuture()
container := &Container{
Future: fut,
}
fn := func(value interface{}, err error) {
container.Value = value
container.Err = err
resolve()
}
return container, fn
}
func Example() {
// Define a value that will be available in the future.
container, resolve := NewContainer()
// Simulate long computation or IO by sleeping before
// resolving the future.
go func() {
time.Sleep(500 * time.Millisecond)
resolve("Hello World!", nil)
}()
// Block until the future is resolved.
<-container.Done()
fmt.Println(container.Value, container.Err)
// output: Hello World! <nil>
}
A full implementation using this technique can be found in github.com/ianlopshire/go-async. Please feel free to open an issue if you have any thoughts or feedback.