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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
✦ That's one of the most important questions for a modern Go developer. The context package, and the ctx variable, are
fundamental to writing robust, scalable, and reliable concurrent programs in Go.
At its core, a context.Context is a standard way to carry deadlines, cancellation signals, and other request-scoped
values across API boundaries and between goroutines.
Think of it as a "control channel" for a request or a task.
The Three Main Jobs of a context.Context
1. Cancellation (The Most Important Job)
This is the primary reason context was created. Imagine you start a complex operation that involves multiple goroutines
and network calls.
* The Problem: What happens if the user who initiated the operation closes their browser or clicks "cancel"? Without a
context, all those background goroutines would keep running, consuming CPU, memory, and network resources, even though
their work is no longer needed. This is a resource leak.
* The `context` Solution: You create a single context object at the beginning of the operation. You then pass this ctx to
every function and goroutine involved in the task. The original function holds a "cancel function." When the user
cancels, this function is called.
* All the downstream functions and goroutines are written to periodically check if the context has been cancelled
(e.-g., select { case <-ctx.Done(): return }).
* As soon as they see the cancellation signal, they stop their work, clean up any resources they were using, and exit
gracefully.
This allows for a clean, cascading shutdown of a whole tree of concurrent operations.
Example:
1 func main() {
2 // Create a context that can be cancelled.
3 ctx, cancel := context.WithCancel(context.Background())
4
5 // Simulate cancelling the operation after 2 seconds.
6 time.AfterFunc(2*time.Second, cancel)
7
8 // Start a long-running operation, passing it the context.
9 longRunningOperation(ctx)
10 }
11
12 func longRunningOperation(ctx context.Context) {
13 for {
14 select {
15 case <-ctx.Done():
16 // The context was cancelled! Clean up and exit.
17 fmt.Println("Operation cancelled. Cleaning up.")
18 return
19 default:
20 // Do a small piece of work.
21 fmt.Println("Working...")
22 time.Sleep(500 * time.Millisecond)
23 }
24 }
25 }
2. Timeouts and Deadlines
This is a specialized form of cancellation. It's extremely common in network clients and servers.
* The Problem: You make a request to a database or another microservice. What if that service is slow or completely
unresponsive? Your request could hang forever, holding up a connection and a goroutine.
* The `context` Solution: You create a context with a timeout.
1 // This context will be automatically cancelled after 3 seconds.
2 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
3 defer cancel() // Good practice to always call cancel
4
5 // Now, make the database call with this context.
6 db.QueryRowContext(ctx, "SELECT ...")
The database/sql driver (and any well-written modern Go library) is designed to accept a ctx. It will monitor the
context and, if the 3-second timeout is reached before the query finishes, it will automatically cancel the database
query on the server and return a timeout error.
3. Request-Scoped Values
This is the third, and least common, use case. A context can carry a lightweight key-value store.
* The Problem: You have a web request, and you need to pass a request ID or a user's authentication token through many
layers of your application (from the HTTP handler to the middleware to the business logic to the database layer).
Passing these values as explicit arguments to every single function would be cumbersome and clutter the function
signatures.
* The `context` Solution: You can store these values in the context at the beginning of the request.
1 // In your middleware or HTTP handler:
2 ctx := context.WithValue(r.Context(), "requestID", "xyz-123")
3 // Now call the next handler with the new context.
4 businessLogic(ctx, ...)
5
6 // In your businessLogic function:
7 func businessLogic(ctx context.Context, ...) {
8 // Retrieve the value later.
9 if reqID, ok := ctx.Value("requestID").(string); ok {
10 log.Printf("Handling request %s", reqID)
11 }
12 }
Important Caveat: This should only be used for request-scoped data that is optional or informational. It should not be
used for required parameters, as that makes your function's dependencies hidden and unclear.
The Rule of Thumb
If a function is doing I/O (network, disk), waiting for anything, or could potentially be slow, it should accept a
context.Context as its first argument. This has become a strong convention in the Go community.
func DoSomething(ctx context.Context, arg1 string, arg2 int) error
|