Request cancellation

A good usage of context is for cancellation and timeout when you're executing an HTTP request using http.Client, which handles the interruption automatically from the context. The following example does exactly that:

func main() {
const addr = "localhost:8080"
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second * 5)
})
go func() {
if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatalln(err)
}
}()
req, _ := http.NewRequest(http.MethodGet, "http://"+addr, nil)
ctx, canc := context.WithTimeout(context.Background(), time.Second*2)
defer canc()
time.Sleep(time.Second)
if _, err := http.DefaultClient.Do(req.WithContext(ctx)); err != nil {
log.Fatalln(err)
}
}

The context cancellation method can also be used to interrupt the current HTTP request that's passed to a client. In a scenario where we are calling different endpoints and returning the first result that's received, it would be a good idea to cancel the other requests.

Let's create an application that runs a query on different search engines and returns the results from the quickest one, cancelling the others. We can create a web server that has a unique endpoint that answers back in 0 to 10 seconds:

const addr = "localhost:8080"
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
d := time.Second * time.Duration(rand.Intn(10))
log.Println("wait", d)
time.Sleep(d)
})
go func() {
if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatalln(err)
}
}()

We can use a cancellable context for the requests, combined with a wait group to synchronize it with the end of the request. Each goroutine will create a request and try to send the result using a channel. Since we are only interested in the first one, we will use  sync.Once to limit it:

ctx, canc := context.WithCancel(context.Background())
ch, o, wg := make(chan int), sync.Once{}, sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func(i int) {
defer wg.Done()
req, _ := http.NewRequest(http.MethodGet, "http://"+addr, nil)
if _, err := http.DefaultClient.Do(req.WithContext(ctx)); err != nil {
log.Println(i, err)
return
}
o.Do(func() { ch <- i })
}(i)
}
log.Println("received", <-ch)
canc()
log.Println("cancelling")
wg.Wait()

When this program runs, we will see that one of the requests is completed successfully and gets sent to the channel, while the others are either cancelled or ignored.