Nowadays, the most common bottleneck in application execution is network requests. The network request only takes a few milliseconds, but the wait for the response takes a hundred times longer. So, if you perform multiple network requests, having them all execute in parallel is the best option to reduce latency. Future/Promise is one of the means to achieve this purpose.
A Future means that you need something "in the future" (usually the result of a network request), but you have to initiate such a request now, and the request will be executed asynchronously. Or to put it another way, you need to perform an asynchronous request in the background.
Future/Promise pattern has corresponding implementations in multiple languages. For example, ES2015 has Promise and async-await, Scala has built-in Future, and finally, Golang has goroutine and channel to achieve similar functions. A simple implementation is given below.
//RequestFuture, http request promise. func RequestFuture(url string) <-chan []byte { c := make(chan []byte, 1) go func() { var body []byte defer func() { c <- body }() res, err := http.Get(url) if err != nil { return } defer res.Body.Close() body, _ = ioutil.ReadAll(res.Body) }() return c } func main() { future := RequestFuture("https://api.github.com/users/octocat/orgs") body := <-future log.Printf("reponse length: %d", len(body)) }
RequestFuture
The method returns a channel. At this time, the http request is still running asynchronously in a goroutine background. The main method can continue to execute other codes, such as triggering other Future
, etc. When results are needed, we need to read the results from the channel. If the http request has not returned, the current goroutine will be blocked until the result is returned.
However, the above method still has some limitations. Error cannot be returned. In the above example, if an error occurs in the http request, the value of body will be nil/empty. However, since the channel can only return one value, you need to create a separate struct to wrap the two returned results.
Result after modification:
// RequestFutureV2 return value and error func RequestFutureV2(url string) func() ([]byte, error) { var body []byte var err error c := make(chan struct{}, 1) go func() { defer close(c) var res *http.Response res, err = http.Get(url) if err != nil { return } defer res.Body.Close() body, err = ioutil.ReadAll(res.Body) }() return func() ([]byte, error) { <-c return body, err } }
This method returns two results, solving the limitations of the first method. When used, it looks like this:
func main() { futureV2 := RequestFutureV2("https://api.github.com/users/octocat/orgs") // not block log.Printf("V2 is this locked again") bodyV2, err := futureV2() // block if err == nil { log.Printf("V2 response length %d\n", len(bodyV2)) } else { log.Printf("V2 error is %v\n", err) } }
The benefit of the above modification is that the futureV2()
method can be called multiple times. And both can return the same result.
However, if you want to use this method to implement many different asynchronous functions, you need to write a lot of extra code. We can write a util method to overcome this difficulty.
// Future boilerplate method 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 } }
When calling the Future
method, many channel tricks in the room will be executed. In order to achieve universal purposes, there is a type conversion from []buyte
->interface{}
->[]byte
. If an error occurs, a runtime panic will be triggered.