The context package in Go allows us to take action(mostly stop work) when the response to an API call is slow. This is one of the most frequent use case.

Let’s consider a scenario where a third party API call(fetchThirdPartyDataWhichCanBeSlow) takes about five seconds to get the response.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
	"context"
	"fmt"
	"log"
	"time"
)

func main() {
	start := time.Now()
	ctx := context.Background()
	userID := 10
	val, err := fetchUserData(ctx, userID)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("result: ", val)
	fmt.Println("took : ", time.Since(start))
}

func fetchUserData(ctx context.Context, userID int) (int, error) {
	val, err := fetchThirdPartyDataWhichCanBeSlow()
	if err != nil {
		return 0, err
	}
	return val, nil
}

func fetchThirdPartyDataWhichCanBeSlow() (int, error) {
	time.Sleep(time.Millisecond * 500)
	return 666, nil
}

If we run the above code, the result would be like this.

% go run context-1.go
result:  666
took :  500.288031ms

Suppose that we want to control the response time limited to less than two seconds. To do so, we can utilize the context package as following.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
	"context"
	"fmt"
	"log"
	"time"
)

func main() {
	start := time.Now()
	ctx := context.Background()
	userID := 10
	val, err := fetchUserData(ctx, userID)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("result: ", val)
	fmt.Println("took : ", time.Since(start))
}

type Response struct {
	value int
	err   error
}

func fetchUserData(ctx context.Context, userID int) (int, error) {
	ctx, cancel := context.WithTimeout(ctx, time.Millisecond*200)
	defer cancel()
	respch := make(chan Response)

	go func() {
		val, err := fetchThirdPartyDataWhichCanBeSlow()
		respch <- Response{
			value: val,
			err:   err,
		}
	}()

	select {
	case <-ctx.Done():
		return 0, fmt.Errorf("fetching data from third party took too long")
	case resp := <-respch:
		return resp.value, resp.err
	}
}

func fetchThirdPartyDataWhichCanBeSlow() (int, error) {
	time.Sleep(time.Millisecond * 500)
	return 666, nil
}

Give your focus on line 29 and 42 where the context magic takes place. On line 29 the context begins measuring time while the third party API call(fetchThirdPartyDataWhichCanBeSlow) is triggered as a goroutine on line 33. At the select statement on line 41, it is determined which case should be returned. In this case, ctx.Done() is triggered earlier than the API call, the code will return 0 with the error message. Below is the proof.

% go run context.go
2023/01/17 00:07:34 fetching data from third party took too long
exit status 1

If we change the API call’s response time from five seconds to one on line 50, it will return the API response without triggering the context’s timeout.

% go run context.go
result:  666
took :  100.677819ms

I reference Anthony GG’s YouTube video to create this post. Please visit his channel for more detailed explanation.


How To Use The Context Package In Golang?