Back in August 2016 Go 1.7 was released, it included among other things a new package called context
. This package was originally implemented in golang.org/x/net/context
but it was copied over to the standard library during this release.
This change made other standard library packages, like net
, net/http
and database/sql
, to be updated to add support for the context
package but without breaking existing APIs. That's why we see functions with similar name and arguments but with an extra context
argument added as the first one, like database/sql.*DB
's Ping
method:
func (db *DB) Ping() error
func (db *DB) PingContext(ctx context.Context) error
This decision was made because of the version one compatibility promise, however behind the scenes most of those methods use context
but with default values, usually context.Background()
:
func (db *DB) Ping() error {
return db.PingContext(context.Background())
}
One important thing to notice about those new methods is the fact that they defined a de-facto convention where if we need to use context.Context
in our functions then it should be the first argument called ctx
.
Let's dive into the context
package.
What is in the context
package?
The context
package defines a type called Context
that is used for deadlines, cancellation signals as well as a way to use request-scoped values.
context.Context
is an interface type that defines four functions:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Depending on how we plan to use context.Context
we may or may not use all the methods:
-
Deadline()
: returns the time when the context should end, if no deadline is set then the returned bool value isfalse
. This function is useful in cases where acontext
is received and we want to calculate if there's enough time to complete the work to be done. -
Done()
: returns a channel that is closed when the context ends, the way this channel is closed depends on how thecontext.Context
was initialized, please refer to the docs for concrete details. -
Err()
: if there was an error then it returns a non-nil error when the returned channel inDone()
was closed, eithercontext.DeadlineExceeded
orcontext.Canceled
, otherwisenil
. -
Value(key interface{})
: it's used to get a request-scoped value stored in the context, used in conjunction withcontext.WithValue
.
It's most likely you're already using context
in one way or another, for example if you're using database/sql
or net/http
; some projects rely heavily on context
to achieve their goal, for example OpenTelemetry uses it intensively for instrumentation.
The code used for this post is available on Github.
Deadlines
Deadlines define a way to indicate something has been completed using time, there are two ways to do that:
-
context.WithDeadline
: uses a concretetime.Time
to indicate when the context should finish, and -
context.WithTimeout
: uses a relativetime.Duration
to indicate when the context should finish.
If you refer to the source code you will notice that context.WithTimeout
uses context.WithDeadline
behind the scenes but adding the timeout to the current time to indicate the deadline:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
Both functions return a new context.Context
as well as a context.CancelFunc
function; this new context.Context
is a copy of the parent one with deadline details attached to it and it's meant to be used as the argument for any subsequent calls that are supposed to be using a deadline.
The function context.CancelFunc
should be called (usually via a defer
) when the corresponding block of code using the new context.Context
is completed, this is to propagate the cancellation to other functions using the context
in case the deadline was reached.
For example:
ctx, cancel := context.WithTimeout(context.Background(), 1 * time.Millisecond)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
The code above will always print context deadline exceeded
because the value we indicated in the call context.WithTimeout
is 1 millisecond, if we modify that value to be something higher than 1 second then it will print out overslept
.
This is because the select
is expecting for one of two channels to receive a message, either the one returned by time
(via time.After()
) or the one indicated in context
(via ctx.Done()
).
Cancellation signals
Cancellation signals define a way to indicate something has been completed by explicitly calling a CancelFunc
function, there is one way to do it:
-
context.WithCancel
: it returns a copy of the context and aCancelFunc
to indicate when to cancel some work.
Similar to the Deadlines, for Cancellation signals a context.Context is returned as well as a CancelFunc
; this function should be explicitly called to indicate when a returned context is canceled to propagate to other functions using the same context the work should stop.
The difference between context.WithCancel
and context.WithDeadline
/context.WithTimeout
is the explicitness, so instead of defining a timeout the CancelFunc
should be called explicitly. In all three cases we always need to call the returned CancelFunc
to properly propagate the cancellation details to other functions using the same context.
The function context.CancelFunc
should be called (usually via a defer
) when the corresponding block of code using the new context.Context
is completed, this is to propagate the cancellation to other functions using the context
in case the deadline was reached.
For example:
ch := make(chan struct{})
run := func(ctx context.Context) {
n := 1
for {
select {
case <-ctx.Done(): // 2. "ctx" is cancelled, we close "ch"
fmt.Println("exiting")
close(ch)
return // returning not to leak the goroutine
default:
time.Sleep(time.Millisecond * 300)
fmt.Println(n)
n++
}
}
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(time.Second * 2)
fmt.Println("goodbye")
cancel() // 1. cancels "ctx"
}()
go run(ctx)
fmt.Println("waiting to cancel...")
<-ch // 3. "ch" is closed, we exit
fmt.Println("bye")
The code above is a bit more elaborated than the one used for Deadlines, the key part is the explicit cancel()
call in the goroutine; that cancel()
call is what in the end stops the run
function that receives the cancelable context.
Request-scoped values
Request-scoped values define a way to set and get values that apply to concrete instances of context.Context, they are meant to be used only during a user request, for example during an HTTP request to pass down information to the subsequent internal calls, there is one way to do it:
-
context.WithValue
: it returns a copy of the context that happens to include the value set.
For example:
ctx := context.WithValue(context.Background(), auth, "Bearer hi")
//-
bearer := ctx.Value(auth)
str, ok := bearer.(string)
if !ok {
log.Fatalln("not a string")
}
fmt.Println("value:", str)
The code above uses both context.WithValue
and context.Context.Value
; context.WithValue
returns a copy of the parent context with the associated value and key, then we can use that returned context and call its method Value
to get the previously assigned value.
The complexity of context.WithValue
is not about the implementation or usage but rather when to make those calls, recall the point of using this function is only for request-scoped values not things meant to live all the time during the execution of the program.
Some common examples include defining a value for JSON Web Tokens or extra headers, in both cases the values are meant to be passed around multiple requests to augment the subsequent requests.
Conclusion
Understanding how to use context.Context
is important when dealing with instructions that require cancellation, for example HTTP requests, database commands or remote produce calls; not only because we need to define a sane value to indicate when to stop a running request to avoid waiting forever but also to identify when a remote call was canceled to properly react to that event.
context.Context
provides a simpler way to deal with multiple goroutines to coordinate their work, to identify timeouts and to determine when those happen. Because instrumentation is an important part of any distributed system, knowing how those values are sent and defined between different calls is useful to understand the flow of our program.
Recommended Reading
If you're looking to expand more about context.Context
, I recommend reading the following links:
Top comments (0)