Futures/Promises in the Land of Golang

These days a common bottleneck in execution time is the network. It simply takes a long time to make the round trip (several milliseconds, versus a 100th of that to process the result). So, if you’re doing multiple network requests, it makes sense to do them in parallel to reduce the overall latency. Futures/promises are one technique to accomplish this.

While there are more technical definitions, a future indicates you’ll need something (usually the result of a network request) in the future and you’d like to ask for it now so it can be fetched in the background. Or in other terms, you’d like to make an asynchronous request in the background.

The future/promise pattern is implemented in many languages. For example, JQuery implements a deferred object and futures are built into Scala. In the land of Golang the goroutine and channel concurrency primitives can be used to build up the functionality. A simple implementation is shown below.

package main import ( "io/ioutil" "log" "net/http" ) func RequestFuture(url string) <-chan []byte { c := make(chan []byte, 1) go func() { var body []byte defer func() { c <- body }() resp, err := http.Get(url) if err != nil { return } defer resp.Body.Close() body, _ = ioutil.ReadAll(resp.Body) }() return c } func main() { future := RequestFuture("http://labs.strava.com") // do many other things, maybe create other futures body := <-future log.Printf("response length: %d", len(body)) }

Here the RequestFuture function returns a channel right away, while the actual http request is done asynchronously in a goroutine. The main function can continue doing other things, like trigger other futures. When the result is needed, we read the result from the channel. If the request isn't finished yet, body := <- future will block until the result is ready. This keeps the intentions of the main function clean and easy to read.

However, there is a limitation: The error value is lost. In the above example, if there is an error, the body will be nil/empty. But, since channels can only return one value, you’ll need to create a separate struct type to wrap both values and return that via the channel.

An alternate solution is to more or less wrap the above example in a function and return that instead.

func RequestFutureFunction(url string) func() ([]byte, error) { var body []byte var err error c := make(chan struct{}, 1) go func() { defer close(c) var resp *http.Response resp, err = http.Get(url) if err != nil { return } defer resp.Body.Close() body, err = ioutil.ReadAll(resp.Body) }() return func() ([]byte, error) { <-c return body, err } }

The function created by RequestFutureFunction returns two values, solving the limitation of the first example. Usage is similar to above.

func main() { future := RequestFutureFunction("http://strava.com") // do many other things, maybe create other futures body, err := future() log.Printf("response length: %d", len(body)) log.Printf("request error: %v", err) }

An added benefit of this approach is future() can be called multiple times and will always return the same result. Something that isn't possible with the first solution, as only one value is pushed onto the returned channel.

But wow, if you want to do this for many different asynchronous functions, you’re in for a lot of boilerplate code. We can fix a bit of that by abstracting the functionality into a helper:

func Future(f func() (interface{}, error)) func() (interface{}, error) { var result interface{} var err error c := make(chan struct{}, 1) go func() { defer close(c) result, err = f() }() return func() (interface{}, error) { <-c return result, err } }

The usage moves some of the http request functionality into the main function, but allows for much cleaner code when calling multiple tasks as futures.

func main() { url := "http://labs.strava.com" future := Future(func() (interface{}, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body) }) // do many other things b, err := future() body, _ := b.([]byte) log.Printf("response length: %d", len(body)) log.Printf("request error: %v", err) }

A lot of the tricky bits of working with channels is reused by calling the Future function. However, to make the use generic, there is a cast from []byte -> interface{} -> []byte. If done incorrectly this would cause a panic at runtime. This is an example of where a "generic" type would help, allowing the compiler to validate things at compile times.

At Strava, we’re attempting to solve this issue by creating the “future functions” using a code generator. For example, if there is a function like:

GetActivity(activityID int64) (*Activity, error)

A “future” version is automatically generated using a template similar to the code above:

GetActivityFuture(activityID int64) func() (*Activity, error)

Once generated, developers using GetActivityFuture get the benefits of type safety at compile time.

Obviously, building a separate code generator has its own overhead, but we’re generating a number of helpers, so it’s not single purpose (more on that in other blog posts).

We strive to solve and iterate on solutions to interesting problems. Our use of futures in Go is just one example. If you’d like to work on more see our job openings.

Gopher by Renee French and licensed under CC BY 3.0 US


Originally published at labs.strava.com by Paul Mach.