Go has a package named context in its standard library. It’s responsibilities are cancelations and carrying request-scoped data (but that doesn’t mean it’s only used in HTTP handlers). I’ve learned it in two parts. For a long time I used it only for storing request-scoped data in HTTP middlewares, but recently I’ve learned the rest of it – (1) how to set up canceling context and (2) acting upon receiving canceling signal from context.
Setting up canceling context
Setting up canceling context is easy. We can use context.WitCancel()
, context.WithTimeout()
or context.WithDeadline()
. All those functions return resulting context and cancel function. When calling, cancel function will cancel context:
- immediatelly if created with WithCancel or
- after elapsed time if created with WithTimeout and WithDeadline.
WithDeadline and WithTimeout differ on how to specify time when cancel function sends signal to context’s Done()
channel. WithTimeout expects time.Duration
while WithDeadline expects time.Time
.
Another important note is that when new context is created from existing, a parent-child link between contexts is created. This allows us if parent is canceled, its childrens are canceled too. So that’s why when we are creating a context we need to specify a base context (in most cases that would be context.Background()
or in HTTP handlers, r.Context()
where r is of *http.Request
).
Acting upon receiving canceling signal from context
Here we acknowledge contex.Done() signal and cancel function/service that could take a long time to execute and for which we are doing all this context thing.
Acknowledging is easy because for this we use built-in select with at least two cases. One case is reserved for Done() and other for our function/service singaling that it’s done (as in it’s result is sent into dedicated custom channel). If our function/service is done before canceling signal then we return its return.
If Done() is triggered first, we also need to cancel our function/service so it stops eating system’s resources. This might be the most challenging part of all. For example canceling a request requires us to get a reference to its transport instance and call its cancelRequest. If we our service/function is implemented with third-party package, explore its API if some sort of cancelation is offered.
Simple example
Below is an example of canceling a request that takes too long to complete and we’d like to cancel it after timeout. We have a server that simulates long running task by sleeping
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"flag" | |
"fmt" | |
"net/http" | |
"time" | |
) | |
func main() { | |
t := flag.Int("timeout", 5, "Sleep duration, in seconds") | |
flag.Parse() | |
http.HandleFunc("/", handler(time.Duration(*t))) | |
fmt.Println("Listening on port 8080") | |
http.ListenAndServe(":8080", nil) | |
} | |
func handler(d time.Duration) http.HandlerFunc { | |
return func(w http.ResponseWriter, r *http.Request) { | |
time.Sleep(d * time.Second) | |
fmt.Fprint(w, "I finished before context deadline exceeded") | |
} | |
} |
Than we have a client which sends request to server. On line 17 we create context with WithTimeout and then defering call to cancel. Function httpDo
performs the actual canceling part.
You can start client with different timeout to see how client’s response differ if timeout is greater/lower than server’s response time.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"flag" | |
"fmt" | |
"io/ioutil" | |
"net/http" | |
"time" | |
) | |
func main() { | |
t := flag.Int("timeout", 2, "Timeout, in seconds") | |
flag.Parse() | |
d := time.Duration(*t) | |
ctx, cancel := context.WithTimeout(context.Background(), d*time.Second) | |
defer cancel() | |
body, err := doWork(ctx) | |
if err != nil { | |
fmt.Println(err) | |
return | |
} | |
fmt.Println(string(body)) | |
} | |
func doWork(ctx context.Context) ([]byte, error) { | |
req, err := http.NewRequest("GET", "http://localhost:8080", nil) | |
if err != nil { | |
return nil, err | |
} | |
var b []byte | |
err = httpDo(ctx, req, func(res *http.Response, err error) error { | |
if err != nil { | |
return err | |
} | |
defer res.Body.Close() | |
if b, err = ioutil.ReadAll(res.Body); err != nil { | |
return err | |
} | |
return nil | |
}) | |
return b, err | |
} | |
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error { | |
tr := &http.Transport{} | |
client := http.Client{Transport: tr} | |
c := make(chan error, 1) | |
go func() { c <- f(client.Do(req)) }() | |
select { | |
case <-ctx.Done(): | |
tr.CancelRequest(req) | |
<-c | |
return ctx.Err() | |
case err := <-c: | |
return err | |
} | |
} |