Understanding The Context Package: Managing Request-scoped Data, Deadlines, And Cancellation With The Context Package
Overview
The context package in Go is essential for managing request-scoped data, timeouts, and cancellations across various parts of your application. This chapter provides an in-depth look into how Go’s context package helps simplify managing concurrent operations. You’ll learn how to propagate values like user data across goroutines, handle timeouts to ensure efficient resource management, and gracefully cancel operations when required. By the end, you’ll understand how to avoid common pitfalls and implement the context package to build robust, scalable Go applications.
Context
Writing concurrent code has many challenges. This chapter will deal specifically with the challenges around:
- Sharing common (contextual) values across concurrent routines
- Canceling concurrent processes
- Timing out concurrent processes
The Problem
To illustrate the difficulties of writing solid concurrent code, we will build a service that queries a database.
The problems we need to solve for are:
- Graceful shutdown of all go routines if the service is told to shut down:
- DB Queries
- Cancel all DB actions related to a specific request
- Share values across concurrent requests such as
Server ID,User ID,Request ID.
While most of these issues can be solved with concurrency primitives, it requires a lot of planning, and in general, is quite difficult and tedious.
Because of this, the context package was introduced in Go 1.7 to solve for these exact scenarios.
The Context Interface
Context can solve for all of the problems described in our problem scenario.
Instead of a concrete type, Context is defined as an interface in Go, and consists of just four methods:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
For most general scenarios, the context package has default implementations for almost all use cases. You can, however, create your own context as well for more specific situations. Projects such as Buffalo do this to make it even easier to work with their framework.
Boilerplate Code
To start with, let’s look at a database query. We’ll use an in-memory sqlite database for this example.
Here is our main section:
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
// Load some sample data
if err := load(db); err != nil {
log.Fatal(err)
}
// Get all users
users, err := GetUsers(db)
if err != nil {
log.Fatal(db)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// section: main
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
// Load some sample data
if err := load(db); err != nil {
log.Fatal(err)
}
// Get all users
users, err := GetUsers(db)
if err != nil {
log.Fatal(db)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}
// section: main
}
func load(db *sql.DB) error {
_, err := db.Exec(`
create table users(id int, name text);
insert into users(id, name) values (1, 'Wilma'), (2, 'Fred'), (3, 'Betty'), (4, 'Barney');
`)
return err
}
// section: users
type User struct {
ID int
Name string
}
func GetUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("select * from users;")
if err != nil {
return nil, err
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, err
}
users = append(users, u)
}
return users, nil
}
// section: users
/*
// section: output
{ID:1 Name:Wilma}
{ID:2 Name:Fred}
{ID:3 Name:Betty}
{ID:4 Name:Barney}
// section: output
*/Here is the code to retrieve the users:
type User struct {
ID int
Name string
}
func GetUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("select * from users;")
if err != nil {
return nil, err
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, err
}
users = append(users, u)
}
return users, nil
}
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// section: main
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
// Load some sample data
if err := load(db); err != nil {
log.Fatal(err)
}
// Get all users
users, err := GetUsers(db)
if err != nil {
log.Fatal(db)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}
// section: main
}
func load(db *sql.DB) error {
_, err := db.Exec(`
create table users(id int, name text);
insert into users(id, name) values (1, 'Wilma'), (2, 'Fred'), (3, 'Betty'), (4, 'Barney');
`)
return err
}
// section: users
type User struct {
ID int
Name string
}
func GetUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("select * from users;")
if err != nil {
return nil, err
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, err
}
users = append(users, u)
}
return users, nil
}
// section: users
/*
// section: output
{ID:1 Name:Wilma}
{ID:2 Name:Fred}
{ID:3 Name:Betty}
{ID:4 Name:Barney}
// section: output
*/Output:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// section: main
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
// Load some sample data
if err := load(db); err != nil {
log.Fatal(err)
}
// Get all users
users, err := GetUsers(db)
if err != nil {
log.Fatal(db)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}
// section: main
}
func load(db *sql.DB) error {
_, err := db.Exec(`
create table users(id int, name text);
insert into users(id, name) values (1, 'Wilma'), (2, 'Fred'), (3, 'Betty'), (4, 'Barney');
`)
return err
}
// section: users
type User struct {
ID int
Name string
}
func GetUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("select * from users;")
if err != nil {
return nil, err
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, err
}
users = append(users, u)
}
return users, nil
}
// section: users
/*
// section: output
{ID:1 Name:Wilma}
{ID:2 Name:Fred}
{ID:3 Name:Betty}
{ID:4 Name:Barney}
// section: output
*/Goroutines
In the previous code, it may not be obvious, but the call to db.Query is just a wrapper for db.QueryContext. By doing this, the db package is now able to work with all subsequent calls with a context present. This allows for specific drivers to be able to use context later for cancellation.
It is worth noting that not all database drivers use context in the same manner. For instance, a call to postgres with context being canceled is handled differently than calling sqlite with the in-memory setup. While it’s not important to understand the exact differences, what is important is to understand they all observe the context for cancellation.
Creating A Context
All contexts start with a context.Background() context. This is just an “empty” context onto which we can create new “child” contexts.
Contexts behave like trees with a top node, context.Background(), and then further sub-nodes.
To create a background context, you can use the following code:
ctx := context.Background()
Context.Background Vs Context.TODO
The context package provides two functions for creating empty contexts:
context.Background()- Use this when you have a clear understanding of what context to use. This is the most common starting point for creating context trees. Use it inmain(), initialization, tests, and as the top-level context for incoming requests.context.TODO()- Use this as a placeholder when you’re unsure which context to use or when the surrounding function has not been updated to accept a context parameter yet. This signals to readers (and linters) that the context usage is incomplete and needs to be addressed.
When to use each:
// ✓ Good: Use Background() in main
func main() {
ctx := context.Background()
// ...
}
// ✓ Good: Use TODO() when refactoring
func legacyFunction() {
// TODO: Update signature to accept context parameter
ctx := context.TODO()
doWork(ctx)
}
Best Practice: If you see context.TODO() in production code during a review, it’s a signal that the code needs refactoring to properly propagate a context from the caller.
Db.QueryContext
Now that we understand that db.Query is going to call db.QueryContext, we can swap out to use db.QueryContext and provide our own context to have more control in the future.
Here is the code using our own context. Note that the behavior has not changed yet, but will allow us to incorporate future changes for handling timeouts and cancellation.
type User struct {
ID int
Name string
}
func GetUsers(db *sql.DB) ([]User, error) {
// we expect someone to give us this context in the future
ctx := context.TODO()
// pass in our context
rows, err := db.QueryContext(ctx, "select * from users;")
if err != nil {
return nil, err
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, err
}
users = append(users, u)
}
return users, nil
}
package main
import (
"context"
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// section: main
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
// Load some sample data
if err := load(db); err != nil {
log.Fatal(err)
}
// Get all users
users, err := GetUsers(db)
if err != nil {
log.Fatal(db)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}
// section: main
}
func load(db *sql.DB) error {
_, err := db.Exec(`
create table users(id int, name text);
insert into users(id, name) values (1, 'Wilma'), (2, 'Fred'), (3, 'Betty'), (4, 'Barney');
`)
return err
}
// section: users
type User struct {
ID int
Name string
}
func GetUsers(db *sql.DB) ([]User, error) {
// we expect someone to give us this context in the future
ctx := context.TODO()
// pass in our context
rows, err := db.QueryContext(ctx, "select * from users;")
if err != nil {
return nil, err
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, err
}
users = append(users, u)
}
return users, nil
}
// section: users
/*
// section: output
{ID:1 Name:Wilma}
{ID:2 Name:Fred}
{ID:3 Name:Betty}
{ID:4 Name:Barney}
// section: output
*/In future revisions, we’ll create the context (or derive the context) from an earlier point in the code, likely refactoring GetUsers to take a context as an argument.
Note: In concurrent programming, it is very common for most calls to take a context as the first argument, resulting in almost all function signatures looking like this:
func SomeFunction(ctx context.Context, ...)
For instance, GetUsers would become:
func GetUsers(ctx context.Context, db *sql.DB) ([]User, error)
Cancellation
While the above code works fine for basic scenarios. What happens if we want to:
- cancel the exiting query?
- need the query to return within a specific deadline, and cancel if it exceeds that deadline?
Context was built with that in mind. In fact, one of the most useful, and common, uses for context.Context is cancellation. Because goroutines are asynchronous in nature, we are also able to use many of the concurrency patterns in conjunction with context. We’ll see this as we continue to modify our project.
Canceling With Contexts
There are two primary ways to cancel with a context. The first is to use a cancel function, and the second is to set up the context with a timeout that will automatically call the cancel function. Determining which method to use will be specific to the use case you are solving for in your program.
To create a cancelable context, you can use context.WithCancel. Because each context attaches to an existing context, we also need to provide an existing context. In most cases, you’ll start with a context.Background when creating your first cancelable context:
ctx, cancel := context.WithCancel(context.Background())
WithCancel returns a copy of parent with a new Done channel. The returned context’s Done channel is closed when the returned cancel function is called or when the parent context’s Done channel is closed, whichever happens first.
Because a context has internal resources that are not freed until the context is canceled, it is common to defer the cancel function. This is safe because calling cancel is an idempotent operation. This means that any subsequent call to cancel will do nothing, thus having no adverse affect on your program.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Using WithCancel
When the cancel function is a called a message is sent to the ctx.Done() channel that we can listen to and then stop our goroutines safely.
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// do some work asynchronously
go func() {
fmt.Println("doing some work...")
// call the cancel function when done working
time.Sleep(2 * time.Second)
fmt.Println("finished with work")
cancel()
}()
// code waits for context to finish via `cancel` function
<-ctx.Done()
fmt.Println("context finished")
// Check the error
err := ctx.Err()
if err != context.Canceled {
log.Fatal(err)
}
}
package main
import (
"context"
"fmt"
"log"
"time"
)
// section: main
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// do some work asynchronously
go func() {
fmt.Println("doing some work...")
// call the cancel function when done working
time.Sleep(2 * time.Second)
fmt.Println("finished with work")
cancel()
}()
// code waits for context to finish via `cancel` function
<-ctx.Done()
fmt.Println("context finished")
// Check the error
err := ctx.Err()
if err != context.Canceled {
log.Fatal(err)
}
}
// section: main
/*
// section: output
doing some work...
finished with work
context finished
// section: output
*/Output:
package main
import (
"context"
"fmt"
"log"
"time"
)
// section: main
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// do some work asynchronously
go func() {
fmt.Println("doing some work...")
// call the cancel function when done working
time.Sleep(2 * time.Second)
fmt.Println("finished with work")
cancel()
}()
// code waits for context to finish via `cancel` function
<-ctx.Done()
fmt.Println("context finished")
// Check the error
err := ctx.Err()
if err != context.Canceled {
log.Fatal(err)
}
}
// section: main
/*
// section: output
doing some work...
finished with work
context finished
// section: output
*/Canceling With Timeouts
Context also provides two methods to allow cancellation of a process within a specified amount of time:
context.WithTimeout
context.WithDeadline
Both do almost the same thing. WithTimeout will cancel the context after a certain amount of time. WithDeadline will cancel the context at a certain time.
They also both provide a cancel function so you can cancel earlier than the timeout if desired.
WithTimeout Vs WithDeadline: When To Use Each
While both achieve automatic cancellation, choosing between them depends on your use case:
Use WithTimeout when you care about duration:
// Cancel after 5 seconds from now
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
Use WithDeadline when you care about a specific time:
// Cancel at exactly 3:00 PM
deadline := time.Date(2025, 1, 15, 15, 0, 0, 0, time.UTC)
ctx, cancel := context.WithDeadline(parent, deadline)
defer cancel()
Common patterns:
- API calls, database queries: Use
WithTimeout- “this operation should complete within 500ms” - Batch jobs, scheduled tasks: Use
WithDeadline- “this job must finish by midnight” - Request handling: Use
WithTimeout- “process this request within 30 seconds” - Time-based coordination: Use
WithDeadline- “all workers must stop at 5:00 PM”
Note: WithTimeout is actually implemented using WithDeadline internally:
// Simplified implementation
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
Using WithTimeout
By using WithTimeout we can ensure we finish within a specified time.
Notice the only line of code that changed was the line in which we created the context. The rest of the code did not need to change at all to observe this new deadline.
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// do some work asynchronously
go func() {
fmt.Println("doing some work...")
time.Sleep(2 * time.Second)
fmt.Println("finished with work")
// call the cancel function when done working
cancel()
}()
// code waits for context to finish via `cancel` function
<-ctx.Done()
fmt.Println("context finished")
// Check the error
err := ctx.Err()
if err != context.Canceled {
log.Fatal(err)
}
}
package main
import (
"context"
"fmt"
"log"
"time"
)
// section: main
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// do some work asynchronously
go func() {
fmt.Println("doing some work...")
time.Sleep(2 * time.Second)
fmt.Println("finished with work")
// call the cancel function when done working
cancel()
}()
// code waits for context to finish via `cancel` function
<-ctx.Done()
fmt.Println("context finished")
// Check the error
err := ctx.Err()
if err != context.Canceled {
log.Fatal(err)
}
}
// section: main
/*
// section: output
doing some work...
context finished
2021/02/08 13:30:38 context deadline exceeded
exit status 1
// section: output
*/Output:
package main
import (
"context"
"fmt"
"log"
"time"
)
// section: main
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// do some work asynchronously
go func() {
fmt.Println("doing some work...")
time.Sleep(2 * time.Second)
fmt.Println("finished with work")
// call the cancel function when done working
cancel()
}()
// code waits for context to finish via `cancel` function
<-ctx.Done()
fmt.Println("context finished")
// Check the error
err := ctx.Err()
if err != context.Canceled {
log.Fatal(err)
}
}
// section: main
/*
// section: output
doing some work...
context finished
2021/02/08 13:30:38 context deadline exceeded
exit status 1
// section: output
*/Notice that we get an error? This is because we specifically timed out before our process could finish.
Context Errors
There are two specific errors that the context package can return. Depending on the needs of your program, they can be useful in making decisions. They are both returned from the Context.Err method.
From the documentation:
Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = errors.New("context canceled")
DeadlineExceeded is the error returned by Context.Err when the context’s deadline passes.
var DeadlineExceeded error = deadlineExceededError{}
If Context.Err() returns nil, this means that the context was neither canceled nor timed out.
Cancellation With A Cause
Context.Err() only tells you that a context was canceled, not why. In a system with multiple cancellation paths, knowing the reason is important for logging, metrics, and error handling.
context.WithCancelCause works like WithCancel but lets you attach a specific error as the cancellation reason. context.Cause(ctx) returns the error passed to cancel. If the context was canceled with a regular CancelFunc (no cause), Cause returns the same value as ctx.Err().
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(fmt.Errorf("database connection lost"))
fmt.Println("ctx.Err(): ", ctx.Err())
fmt.Println("context.Cause(): ", context.Cause(ctx))
}
package main
import (
"context"
"fmt"
)
// section: main
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(fmt.Errorf("database connection lost"))
fmt.Println("ctx.Err(): ", ctx.Err())
fmt.Println("context.Cause(): ", context.Cause(ctx))
}
// section: main
// section: output
/*
ctx.Err(): context canceled
context.Cause(): database connection lost
*/
// section: outputOutput:
package main
import (
"context"
"fmt"
)
// section: main
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(fmt.Errorf("database connection lost"))
fmt.Println("ctx.Err(): ", ctx.Err())
fmt.Println("context.Cause(): ", context.Cause(ctx))
}
// section: main
// section: output
/*
ctx.Err(): context canceled
context.Cause(): database connection lost
*/
// section: outputWithTimeoutCause And WithDeadlineCause
The same pattern extends to timeouts and deadlines. By default, when a timeout expires, context.Cause returns context.DeadlineExceeded, which tells you nothing about which timeout fired. WithTimeoutCause and WithDeadlineCause let you attach a specific error so you know exactly which layer timed out.
func main() {
// Without cause
ctx1, cancel1 := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel1()
<-ctx1.Done()
fmt.Println("WithTimeout:")
fmt.Println(" ctx.Err(): ", ctx1.Err())
fmt.Println(" context.Cause(): ", context.Cause(ctx1))
// With cause
ctx2, cancel2 := context.WithTimeoutCause(context.Background(), 1*time.Millisecond,
fmt.Errorf("database query exceeded limit"))
defer cancel2()
<-ctx2.Done()
fmt.Println("\nWithTimeoutCause:")
fmt.Println(" ctx.Err(): ", ctx2.Err())
fmt.Println(" context.Cause(): ", context.Cause(ctx2))
}
package main
import (
"context"
"fmt"
"time"
)
// section: main
func main() {
// Without cause
ctx1, cancel1 := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel1()
<-ctx1.Done()
fmt.Println("WithTimeout:")
fmt.Println(" ctx.Err(): ", ctx1.Err())
fmt.Println(" context.Cause(): ", context.Cause(ctx1))
// With cause
ctx2, cancel2 := context.WithTimeoutCause(context.Background(), 1*time.Millisecond,
fmt.Errorf("database query exceeded limit"))
defer cancel2()
<-ctx2.Done()
fmt.Println("\nWithTimeoutCause:")
fmt.Println(" ctx.Err(): ", ctx2.Err())
fmt.Println(" context.Cause(): ", context.Cause(ctx2))
}
// section: main
// section: output
/*
WithTimeout:
ctx.Err(): context deadline exceeded
context.Cause(): context deadline exceeded
WithTimeoutCause:
ctx.Err(): context deadline exceeded
context.Cause(): database query exceeded limit
*/
// section: outputOutput:
/*
WithTimeout:
ctx.Err(): context deadline exceeded
context.Cause(): context deadline exceeded
WithTimeoutCause:
ctx.Err(): context deadline exceeded
context.Cause(): database query exceeded limit
*/package main
import (
"context"
"fmt"
"time"
)
// section: main
func main() {
// Without cause
ctx1, cancel1 := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel1()
<-ctx1.Done()
fmt.Println("WithTimeout:")
fmt.Println(" ctx.Err(): ", ctx1.Err())
fmt.Println(" context.Cause(): ", context.Cause(ctx1))
// With cause
ctx2, cancel2 := context.WithTimeoutCause(context.Background(), 1*time.Millisecond,
fmt.Errorf("database query exceeded limit"))
defer cancel2()
<-ctx2.Done()
fmt.Println("\nWithTimeoutCause:")
fmt.Println(" ctx.Err(): ", ctx2.Err())
fmt.Println(" context.Cause(): ", context.Cause(ctx2))
}
// section: main
// section: output
/*
WithTimeout:
ctx.Err(): context deadline exceeded
context.Cause(): context deadline exceeded
WithTimeoutCause:
ctx.Err(): context deadline exceeded
context.Cause(): database query exceeded limit
*/
// section: outputUpdating Our Program
Now that we have a better understanding of Context, we can make some improvements in our program.
We’ll make the following changes:
- Create a top level context that the entire program will use.
- Derive all new contexts from our top level context.
- Create timeouts where appropriate with our contexts.
Main
In the main function, we’ll create a top level context with a cancel function. We’ll pass this context to all other calls.
We’ll also run our code in a goroutine. This will allow us to add a future timeout to the entire program if we desired with only one line of code.
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Load some sample data
if err := load(ctx, db); err != nil {
log.Fatal(err)
}
// Get all users
users, err := GetUsers(ctx, db)
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// section: main
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Load some sample data
if err := load(ctx, db); err != nil {
log.Fatal(err)
}
// Get all users
users, err := GetUsers(ctx, db)
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}
// section: main
}
// section: load
func load(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx,
`
create table users(id int, name text);
insert into users(id, name) values (1, 'Wilma'), (2, 'Fred'), (3, 'Betty'), (4, 'Barney');
`,
)
return err
}
// section: load
// section: users
type User struct {
ID int
Name string
}
func GetUsers(ctx context.Context, db *sql.DB) ([]User, error) {
// Put a timeout on our query
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
// create an error channel to return any errors in our goroutine
ec := make(chan error, 1)
// create a channel to retrieve our result
result := make(chan []User, 1)
// launch the code in a go routine
go func() {
// pass in our context
rows, err := db.QueryContext(ctx, "select * from users;")
if err != nil {
ec <- err
return
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
ec <- err
return
}
users = append(users, u)
}
result <- users
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-ec:
return nil, err
case u := <-result:
return u, nil
}
}
// section: usersLoad
The load function now takes a context. As such, we want to make sure we will return early if our parent context is canceled:
func load(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx,
`
create table users(id int, name text);
insert into users(id, name) values (1, 'Wilma'), (2, 'Fred'), (3, 'Betty'), (4, 'Barney');
`,
)
return err
}
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// section: main
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Load some sample data
if err := load(ctx, db); err != nil {
log.Fatal(err)
}
// Get all users
users, err := GetUsers(ctx, db)
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}
// section: main
}
// section: load
func load(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx,
`
create table users(id int, name text);
insert into users(id, name) values (1, 'Wilma'), (2, 'Fred'), (3, 'Betty'), (4, 'Barney');
`,
)
return err
}
// section: load
// section: users
type User struct {
ID int
Name string
}
func GetUsers(ctx context.Context, db *sql.DB) ([]User, error) {
// Put a timeout on our query
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
// create an error channel to return any errors in our goroutine
ec := make(chan error, 1)
// create a channel to retrieve our result
result := make(chan []User, 1)
// launch the code in a go routine
go func() {
// pass in our context
rows, err := db.QueryContext(ctx, "select * from users;")
if err != nil {
ec <- err
return
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
ec <- err
return
}
users = append(users, u)
}
result <- users
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-ec:
return nil, err
case u := <-result:
return u, nil
}
}
// section: usersGetUsers
type User struct {
ID int
Name string
}
func GetUsers(ctx context.Context, db *sql.DB) ([]User, error) {
// Put a timeout on our query
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
// create an error channel to return any errors in our goroutine
ec := make(chan error, 1)
// create a channel to retrieve our result
result := make(chan []User, 1)
// launch the code in a go routine
go func() {
// pass in our context
rows, err := db.QueryContext(ctx, "select * from users;")
if err != nil {
ec <- err
return
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
ec <- err
return
}
users = append(users, u)
}
result <- users
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-ec:
return nil, err
case u := <-result:
return u, nil
}
}
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// section: main
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Load some sample data
if err := load(ctx, db); err != nil {
log.Fatal(err)
}
// Get all users
users, err := GetUsers(ctx, db)
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}
// section: main
}
// section: load
func load(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx,
`
create table users(id int, name text);
insert into users(id, name) values (1, 'Wilma'), (2, 'Fred'), (3, 'Betty'), (4, 'Barney');
`,
)
return err
}
// section: load
// section: users
type User struct {
ID int
Name string
}
func GetUsers(ctx context.Context, db *sql.DB) ([]User, error) {
// Put a timeout on our query
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
// create an error channel to return any errors in our goroutine
ec := make(chan error, 1)
// create a channel to retrieve our result
result := make(chan []User, 1)
// launch the code in a go routine
go func() {
// pass in our context
rows, err := db.QueryContext(ctx, "select * from users;")
if err != nil {
ec <- err
return
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
ec <- err
return
}
users = append(users, u)
}
result <- users
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-ec:
return nil, err
case u := <-result:
return u, nil
}
}
// section: usersFinished Program
Here is the entire set of code, now written to make use of the context package:
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Load some sample data
if err := load(ctx, db); err != nil {
log.Fatal(err)
}
// Get all users
users, err := GetUsers(ctx, db)
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}
}
func load(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx,
`
create table users(id int, name text);
insert into users(id, name) values (1, 'Wilma'), (2, 'Fred'), (3, 'Betty'), (4, 'Barney');
`,
)
return err
}
type User struct {
ID int
Name string
}
func GetUsers(ctx context.Context, db *sql.DB) ([]User, error) {
// Put a timeout on our query
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
// create an error channel to return any errors in our goroutine
ec := make(chan error, 1)
// create a channel to retrieve our result
result := make(chan []User, 1)
// launch the code in a go routine
go func() {
// pass in our context
rows, err := db.QueryContext(ctx, "select * from users;")
if err != nil {
ec <- err
return
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
ec <- err
return
}
users = append(users, u)
}
result <- users
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-ec:
return nil, err
case u := <-result:
return u, nil
}
}
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// section: main
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Load some sample data
if err := load(ctx, db); err != nil {
log.Fatal(err)
}
// Get all users
users, err := GetUsers(ctx, db)
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}
// section: main
}
// section: load
func load(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx,
`
create table users(id int, name text);
insert into users(id, name) values (1, 'Wilma'), (2, 'Fred'), (3, 'Betty'), (4, 'Barney');
`,
)
return err
}
// section: load
// section: users
type User struct {
ID int
Name string
}
func GetUsers(ctx context.Context, db *sql.DB) ([]User, error) {
// Put a timeout on our query
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
// create an error channel to return any errors in our goroutine
ec := make(chan error, 1)
// create a channel to retrieve our result
result := make(chan []User, 1)
// launch the code in a go routine
go func() {
// pass in our context
rows, err := db.QueryContext(ctx, "select * from users;")
if err != nil {
ec <- err
return
}
users := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
ec <- err
return
}
users = append(users, u)
}
result <- users
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-ec:
return nil, err
case u := <-result:
return u, nil
}
}
// section: usersSummary
As we can see, when we introduce a context, not only do we have to observe the context, but we end up running more of our code within localized goroutines to ensure that we properly observe any context timeouts or cancellations that may occur upstream. This is an important pattern with concurrent programming and context.
While this approach may seem tedious at first, it will offer the greatest flexibility long term as you implement SLA’s for future performance.
Production Scenarios
While the example we just looked at only had really three layers of context:
- Main routine
- User Lookup (
GetUsers) - DB Query
It’s common to have even more layers. For example, you may have a RESTful web service, in which case the same context would have the following layers it passes through:
- Main Routine
- HTTP Service Routine
- HTTP Handler Routine
- User Lookup (
GetUsers) - DB Query
Each level may cause a timeout/deadline to occur. Each level may need it’s own ability to cancel.
Using a context allows this type of program to share values, cancellation, and deadlines through each layer.
As a bonus, you now have the ability to cancel the top level context, and have all layers and services shut down gracefully.
Signal-Based Cancellation With Os/signal
One of the most common production patterns is gracefully shutting down your application when receiving OS signals (like SIGINT from Ctrl+C or SIGTERM from container orchestrators).
Go 1.16 introduced signal.NotifyContext, which elegantly combines signal handling with context cancellation.
Signal.NotifyContext
Before signal.NotifyContext, signal handling required manual channel management:
Old Pattern (Pre-Go 1.16):
// ❌ Old: Manual signal handling
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
<-sigCh
cancel()
}()
Modern Pattern (Go 1.16+):
// ✓ Good: Using signal.NotifyContext
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
The stop function de-registers the signal handler and releases resources. Always defer it to prevent resource leaks.
Graceful Shutdown Pattern
Here’s a complete pattern for gracefully shutting down an HTTP server:
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Create a context that will be canceled on SIGINT or SIGTERM
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// Create HTTP server
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Simulate some work
time.Sleep(2 * time.Second)
fmt.Fprintf(w, "Request completed at %s\n", time.Now().Format(time.RFC3339))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
serverErrors := make(chan error, 1)
// Start server in a goroutine
go func() {
log.Println("Server starting on :8080")
serverErrors <- srv.ListenAndServe()
}()
select {
case <-ctx.Done():
log.Println("Shutdown signal received")
case err := <-serverErrors:
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("Server exited with error: %v", err)
}
return
}
// Stop receiving any more signals (allow force quit on second Ctrl+C)
stop()
// Create a context with timeout for the shutdown phase
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
log.Println("Gracefully shutting down server (30s timeout)...")
// Attempt graceful shutdown
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("Server forced to shutdown: %v", err)
return
}
if err := <-serverErrors; err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("Server exited with error after shutdown: %v", err)
return
}
log.Println("Server stopped gracefully")
}package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Create a context that will be canceled on SIGINT or SIGTERM
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// Create HTTP server
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Simulate some work
time.Sleep(2 * time.Second)
fmt.Fprintf(w, "Request completed at %s\n", time.Now().Format(time.RFC3339))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
serverErrors := make(chan error, 1)
// Start server in a goroutine
go func() {
log.Println("Server starting on :8080")
serverErrors <- srv.ListenAndServe()
}()
select {
case <-ctx.Done():
log.Println("Shutdown signal received")
case err := <-serverErrors:
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("Server exited with error: %v", err)
}
return
}
// Stop receiving any more signals (allow force quit on second Ctrl+C)
stop()
// Create a context with timeout for the shutdown phase
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
log.Println("Gracefully shutting down server (30s timeout)...")
// Attempt graceful shutdown
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("Server forced to shutdown: %v", err)
return
}
if err := <-serverErrors; err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("Server exited with error after shutdown: %v", err)
return
}
log.Println("Server stopped gracefully")
}Key points:
- Primary context from
signal.NotifyContext- Triggers on OS signals - Shutdown timeout - Separate context for the shutdown phase (e.g., 30 seconds to drain connections)
- Two-phase shutdown:
- Phase 1: Stop accepting new requests
- Phase 2: Complete in-flight requests within timeout
- Error handling - Distinguish between clean shutdown and errors
Signal Propagation In Services
In production services, you typically want to:
- Create a signal-aware context at the application root
- Pass this context to all long-running operations
- Each operation observes
ctx.Done()for cancellation
Example with multiple workers:
package main
import (
"context"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func main() {
// Create signal-aware context at application root
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
log.Println("Starting application with 3 workers")
log.Println("Press Ctrl+C to trigger graceful shutdown")
var wg sync.WaitGroup
// Start multiple workers that all observe the same context
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, &wg, i)
}
// Wait for all workers to complete
wg.Wait()
log.Println("All workers stopped, application exiting")
}
func worker(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
log.Printf("Worker %d: Started", id)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
// Context canceled due to signal
log.Printf("Worker %d: Received shutdown signal, cleaning up...", id)
// Simulate cleanup work
time.Sleep(500 * time.Millisecond)
log.Printf("Worker %d: Stopped", id)
return
case t := <-ticker.C:
// Normal work
log.Printf("Worker %d: Working at %s", id, t.Format("15:04:05"))
}
}
}
/*
2025/01/15 10:30:00 Starting application with 3 workers
2025/01/15 10:30:00 Press Ctrl+C to trigger graceful shutdown
2025/01/15 10:30:00 Worker 1: Started
2025/01/15 10:30:00 Worker 2: Started
2025/01/15 10:30:00 Worker 3: Started
2025/01/15 10:30:01 Worker 1: Working at 10:30:01
2025/01/15 10:30:01 Worker 2: Working at 10:30:01
2025/01/15 10:30:01 Worker 3: Working at 10:30:01
2025/01/15 10:30:02 Worker 1: Working at 10:30:02
2025/01/15 10:30:02 Worker 2: Working at 10:30:02
2025/01/15 10:30:02 Worker 3: Working at 10:30:02
^C
2025/01/15 10:30:03 Worker 2: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 1: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 3: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 2: Stopped
2025/01/15 10:30:03 Worker 1: Stopped
2025/01/15 10:30:03 Worker 3: Stopped
2025/01/15 10:30:03 All workers stopped, application exiting
*/package main
import (
"context"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// section: code
func main() {
// Create signal-aware context at application root
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
log.Println("Starting application with 3 workers")
log.Println("Press Ctrl+C to trigger graceful shutdown")
var wg sync.WaitGroup
// Start multiple workers that all observe the same context
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, &wg, i)
}
// Wait for all workers to complete
wg.Wait()
log.Println("All workers stopped, application exiting")
}
func worker(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
log.Printf("Worker %d: Started", id)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
// Context canceled due to signal
log.Printf("Worker %d: Received shutdown signal, cleaning up...", id)
// Simulate cleanup work
time.Sleep(500 * time.Millisecond)
log.Printf("Worker %d: Stopped", id)
return
case t := <-ticker.C:
// Normal work
log.Printf("Worker %d: Working at %s", id, t.Format("15:04:05"))
}
}
}
// section: output
/*
2025/01/15 10:30:00 Starting application with 3 workers
2025/01/15 10:30:00 Press Ctrl+C to trigger graceful shutdown
2025/01/15 10:30:00 Worker 1: Started
2025/01/15 10:30:00 Worker 2: Started
2025/01/15 10:30:00 Worker 3: Started
2025/01/15 10:30:01 Worker 1: Working at 10:30:01
2025/01/15 10:30:01 Worker 2: Working at 10:30:01
2025/01/15 10:30:01 Worker 3: Working at 10:30:01
2025/01/15 10:30:02 Worker 1: Working at 10:30:02
2025/01/15 10:30:02 Worker 2: Working at 10:30:02
2025/01/15 10:30:02 Worker 3: Working at 10:30:02
^C
2025/01/15 10:30:03 Worker 2: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 1: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 3: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 2: Stopped
2025/01/15 10:30:03 Worker 1: Stopped
2025/01/15 10:30:03 Worker 3: Stopped
2025/01/15 10:30:03 All workers stopped, application exiting
*/Output when pressing Ctrl+C:
/*
2025/01/15 10:30:00 Starting application with 3 workers
2025/01/15 10:30:00 Press Ctrl+C to trigger graceful shutdown
2025/01/15 10:30:00 Worker 1: Started
2025/01/15 10:30:00 Worker 2: Started
2025/01/15 10:30:00 Worker 3: Started
2025/01/15 10:30:01 Worker 1: Working at 10:30:01
2025/01/15 10:30:01 Worker 2: Working at 10:30:01
2025/01/15 10:30:01 Worker 3: Working at 10:30:01
2025/01/15 10:30:02 Worker 1: Working at 10:30:02
2025/01/15 10:30:02 Worker 2: Working at 10:30:02
2025/01/15 10:30:02 Worker 3: Working at 10:30:02
^C
2025/01/15 10:30:03 Worker 2: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 1: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 3: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 2: Stopped
2025/01/15 10:30:03 Worker 1: Stopped
2025/01/15 10:30:03 Worker 3: Stopped
2025/01/15 10:30:03 All workers stopped, application exiting
*/package main
import (
"context"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// section: code
func main() {
// Create signal-aware context at application root
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
log.Println("Starting application with 3 workers")
log.Println("Press Ctrl+C to trigger graceful shutdown")
var wg sync.WaitGroup
// Start multiple workers that all observe the same context
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, &wg, i)
}
// Wait for all workers to complete
wg.Wait()
log.Println("All workers stopped, application exiting")
}
func worker(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
log.Printf("Worker %d: Started", id)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
// Context canceled due to signal
log.Printf("Worker %d: Received shutdown signal, cleaning up...", id)
// Simulate cleanup work
time.Sleep(500 * time.Millisecond)
log.Printf("Worker %d: Stopped", id)
return
case t := <-ticker.C:
// Normal work
log.Printf("Worker %d: Working at %s", id, t.Format("15:04:05"))
}
}
}
// section: output
/*
2025/01/15 10:30:00 Starting application with 3 workers
2025/01/15 10:30:00 Press Ctrl+C to trigger graceful shutdown
2025/01/15 10:30:00 Worker 1: Started
2025/01/15 10:30:00 Worker 2: Started
2025/01/15 10:30:00 Worker 3: Started
2025/01/15 10:30:01 Worker 1: Working at 10:30:01
2025/01/15 10:30:01 Worker 2: Working at 10:30:01
2025/01/15 10:30:01 Worker 3: Working at 10:30:01
2025/01/15 10:30:02 Worker 1: Working at 10:30:02
2025/01/15 10:30:02 Worker 2: Working at 10:30:02
2025/01/15 10:30:02 Worker 3: Working at 10:30:02
^C
2025/01/15 10:30:03 Worker 2: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 1: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 3: Received shutdown signal, cleaning up...
2025/01/15 10:30:03 Worker 2: Stopped
2025/01/15 10:30:03 Worker 1: Stopped
2025/01/15 10:30:03 Worker 3: Stopped
2025/01/15 10:30:03 All workers stopped, application exiting
*/All workers receive the cancellation signal and shut down gracefully.
Production Best Practices
1. Always use a shutdown timeout:
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done() // Wait for signal
// Create a separate timeout context for shutdown
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("Server shutdown failed: %v", err)
}
2. Handle multiple shutdown signals:
// First signal: graceful shutdown
// Second signal: force exit
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done()
log.Println("Shutting down gracefully, press Ctrl+C again to force")
stop() // De-register so second signal works normally
3. Coordinate multiple services:
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// Start all services with the same context
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
g.Go(func() error { return grpcServer.Serve(grpcListener) })
g.Go(func() error { return messageConsumer.Run(ctx) })
g.Go(func() error {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = httpServer.Shutdown(shutdownCtx)
grpcServer.GracefulStop()
messageConsumer.Stop()
return ctx.Err()
})
// Wait for signal or any service to fail
if err := g.Wait(); err != nil && !errors.Is(err, context.Canceled) {
log.Fatalf("Service error: %v", err)
}
When To Use Signal.NotifyContext
✓ Use it for:
- Application-level shutdown coordination
- Server lifecycle management
- Long-running background workers
- Batch processing jobs that should be interruptible
✗ Don’t use it for:
- Request-scoped contexts (use
http.Request.Context()) - Short-lived operations (use
WithTimeoutinstead) - Library code (let the caller control signal handling)
Best Practice: Create the signal context once in main() and derive all other contexts from it. This ensures a single signal can cascade through your entire application.
Code Walkthrough (Optional)
Picking up where we left off with the concurrent solution, lets see how we could use context in place of some channels:
Note: Run the program with go run main.go "$(< sites.txt)" to see the results.
package main
import (
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
// create a custom type to receive our responses
type resp struct {
site string
duration time.Duration
err error
}
func main() {
// Load sites from a file
args := os.Args[1:]
// Args might be a single string with line returns to delimit sites, break them apart
if len(args) == 1 {
args = strings.Split(args[0], "\n")
}
// Create a channel to send all sites to
// We'll create it buffered so that we throttle how much many go routines will be processing at any given time
sites := make(chan string, 30)
// create an unbuffered channel to receive on
// because we don't have contention or latency on the end of our
// process, there is no need to buffer this channel
responses := make(chan resp)
now := time.Now()
// launch the same number of go routines that we have a buffer size for
for i := 0; i < cap(sites); i++ {
go get(sites, responses)
}
// feed all of our data into the send channel
// this channel will block when it's full, and unblock
// when space becomes available
// We will do this in a goroutine so we don't block execution
go func() {
for _, s := range args {
sites <- s
}
// ensure that any open go routines close down
close(sites)
}()
// read back our responses
for i := 0; i < len(args); i++ {
r := <-responses
if r.err != nil {
log.Printf("failed to retrieve %s: %s\n", r.site, r.err)
continue
}
log.Printf("Retrieved site %s in %s\n", r.site, r.duration)
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(sites chan string, responses chan resp) {
// range through the receiving channel. When this channel is closed
// all go routines will terminate
for s := range sites {
now := time.Now()
// pick a magic timeout duration
td := 2 * time.Second
// create a timeout.
timeout := time.NewTimer(td)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to to receive the response on locally
// make sure it's buffered so you don't leak a goroutine
work := make(chan resp, 1)
// retrieve the site in a goroutine to avoid blocking
go func() {
// retrieve the website
_, err := http.Get(s)
work <- resp{site: s, duration: time.Since(now), err: err}
}()
select {
case r := <-work:
// retrieve the value from our local function and pass it through to our send channel
responses <- r
case <-timeout.C:
responses <- resp{site: s, err: fmt.Errorf("timed out after %s", td)}
}
}
}
// section: code
package main
import (
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
// create a custom type to receive our responses
type resp struct {
site string
duration time.Duration
err error
}
func main() {
// Load sites from a file
args := os.Args[1:]
// Args might be a single string with line returns to delimit sites, break them apart
if len(args) == 1 {
args = strings.Split(args[0], "\n")
}
// Create a channel to send all sites to
// We'll create it buffered so that we throttle how much many go routines will be processing at any given time
sites := make(chan string, 30)
// create an unbuffered channel to receive on
// because we don't have contention or latency on the end of our
// process, there is no need to buffer this channel
responses := make(chan resp)
now := time.Now()
// launch the same number of go routines that we have a buffer size for
for i := 0; i < cap(sites); i++ {
go get(sites, responses)
}
// feed all of our data into the send channel
// this channel will block when it's full, and unblock
// when space becomes available
// We will do this in a goroutine so we don't block execution
go func() {
for _, s := range args {
sites <- s
}
// ensure that any open go routines close down
close(sites)
}()
// read back our responses
for i := 0; i < len(args); i++ {
r := <-responses
if r.err != nil {
log.Printf("failed to retrieve %s: %s\n", r.site, r.err)
continue
}
log.Printf("Retrieved site %s in %s\n", r.site, r.duration)
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(sites chan string, responses chan resp) {
// range through the receiving channel. When this channel is closed
// all go routines will terminate
for s := range sites {
now := time.Now()
// pick a magic timeout duration
td := 2 * time.Second
// create a timeout.
timeout := time.NewTimer(td)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to to receive the response on locally
// make sure it's buffered so you don't leak a goroutine
work := make(chan resp, 1)
// retrieve the site in a goroutine to avoid blocking
go func() {
// retrieve the website
_, err := http.Get(s)
work <- resp{site: s, duration: time.Since(now), err: err}
}()
select {
case r := <-work:
// retrieve the value from our local function and pass it through to our send channel
responses <- r
case <-timeout.C:
responses <- resp{site: s, err: fmt.Errorf("timed out after %s", td)}
}
}
}
// section: codeAdding A Context
Adding a context makes the timeout a little easier in the get function.
Note: Run the program with go run main.go "$(< sites.txt)" to see the results.
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
// create a custom type to receive our responses
type resp struct {
site string
duration time.Duration
err error
}
func main() {
// Load sites from a file
args := os.Args[1:]
// Args might be a single string with line returns to delimit sites, break them apart
if len(args) == 1 {
args = strings.Split(args[0], "\n")
}
// Create a channel to send all sites to
// We'll create it buffered so that we throttle how much many go routines will be processing at any given time
sites := make(chan string, 30)
// create an unbuffered channel to receive on
// because we don't have contention or latency on the end of our
// process, there is no need to buffer this channel
responses := make(chan resp)
now := time.Now()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// launch the same number of go routines that we have a buffer size for
for i := 0; i < cap(sites); i++ {
go get(ctx, sites, responses)
}
// feed all of our data into the send channel
// this channel will block when it's full, and unblock
// when space becomes available
// We will do this in a goroutine so we don't block execution
go func() {
for _, s := range args {
sites <- s
}
// ensure that any open go routines close down
close(sites)
}()
// launch the reads in a goroutine to not block context cancellation
go func() {
// signal program has finished
defer cancel()
// read back our responses
for i := 0; i < len(args); i++ {
r := <-responses
if r.err != nil {
log.Printf("failed to retrieve %s: %s\n", r.site, r.err)
continue
}
log.Printf("Retrieved site %s in %s\n", r.site, r.duration)
}
}()
// wait for program to finish
<-ctx.Done()
if err := ctx.Err(); err != context.Canceled {
fmt.Printf("\nprogram timed out: %s\n", err)
return
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(ctx context.Context, sites chan string, responses chan resp) {
// range through the receiving channel. When this channel is closed
// all go routines will terminate
for s := range sites {
now := time.Now()
// pick a magic timeout duration
td := 2 * time.Second
// create a timeout.
ctx, cancel := context.WithTimeout(ctx, td)
defer cancel()
// create a channel to to receive the response on locally
// make sure it's buffered so you don't leak a goroutine
work := make(chan resp, 1)
// retrieve the site in a goroutine to avoid blocking
go func() {
// retrieve the website
_, err := http.Get(s)
work <- resp{site: s, duration: time.Since(now), err: err}
}()
select {
case r := <-work:
// retrieve the value from our local function and pass it through to our send channel
responses <- r
case <-ctx.Done():
responses <- resp{site: s, err: fmt.Errorf("timed out after %s", td)}
}
}
}
// section: code
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
// create a custom type to receive our responses
type resp struct {
site string
duration time.Duration
err error
}
func main() {
// Load sites from a file
args := os.Args[1:]
// Args might be a single string with line returns to delimit sites, break them apart
if len(args) == 1 {
args = strings.Split(args[0], "\n")
}
// Create a channel to send all sites to
// We'll create it buffered so that we throttle how much many go routines will be processing at any given time
sites := make(chan string, 30)
// create an unbuffered channel to receive on
// because we don't have contention or latency on the end of our
// process, there is no need to buffer this channel
responses := make(chan resp)
now := time.Now()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// launch the same number of go routines that we have a buffer size for
for i := 0; i < cap(sites); i++ {
go get(ctx, sites, responses)
}
// feed all of our data into the send channel
// this channel will block when it's full, and unblock
// when space becomes available
// We will do this in a goroutine so we don't block execution
go func() {
for _, s := range args {
sites <- s
}
// ensure that any open go routines close down
close(sites)
}()
// launch the reads in a goroutine to not block context cancellation
go func() {
// signal program has finished
defer cancel()
// read back our responses
for i := 0; i < len(args); i++ {
r := <-responses
if r.err != nil {
log.Printf("failed to retrieve %s: %s\n", r.site, r.err)
continue
}
log.Printf("Retrieved site %s in %s\n", r.site, r.duration)
}
}()
// wait for program to finish
<-ctx.Done()
if err := ctx.Err(); err != context.Canceled {
fmt.Printf("\nprogram timed out: %s\n", err)
return
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(ctx context.Context, sites chan string, responses chan resp) {
// range through the receiving channel. When this channel is closed
// all go routines will terminate
for s := range sites {
now := time.Now()
// pick a magic timeout duration
td := 2 * time.Second
// create a timeout.
ctx, cancel := context.WithTimeout(ctx, td)
defer cancel()
// create a channel to to receive the response on locally
// make sure it's buffered so you don't leak a goroutine
work := make(chan resp, 1)
// retrieve the site in a goroutine to avoid blocking
go func() {
// retrieve the website
_, err := http.Get(s)
work <- resp{site: s, duration: time.Since(now), err: err}
}()
select {
case r := <-work:
// retrieve the value from our local function and pass it through to our send channel
responses <- r
case <-ctx.Done():
responses <- resp{site: s, err: fmt.Errorf("timed out after %s", td)}
}
}
}
// section: codeAdding Program Timeout
Now that we have a context in place, we can also tell the program we want to end after 10 seconds for the entire program.
Note: Run the program with go run main.go "$(< sites.txt)" to see the results.
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
// create a custom type to receive our responses
type resp struct {
site string
duration time.Duration
err error
}
func main() {
// Load sites from a file
args := os.Args[1:]
// Args might be a single string with line returns to delimit sites, break them apart
if len(args) == 1 {
args = strings.Split(args[0], "\n")
}
// Create a channel to send all sites to
// We'll create it buffered so that we throttle how much many go routines will be processing at any given time
sites := make(chan string, 30)
// create an unbuffered channel to receive on
// because we don't have contention or latency on the end of our
// process, there is no need to buffer this channel
responses := make(chan resp)
now := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// launch the same number of go routines that we have a buffer size for
for i := 0; i < cap(sites); i++ {
go get(ctx, sites, responses)
}
// feed all of our data into the send channel
// this channel will block when it's full, and unblock
// when space becomes available
// We will do this in a goroutine so we don't block execution
go func() {
for _, s := range args {
sites <- s
}
// ensure that any open go routines close down
close(sites)
}()
// launch the reads in a goroutine to not block context cancellation
go func() {
// signal program has finished
defer cancel()
// read back our responses
for i := 0; i < len(args); i++ {
r := <-responses
if r.err != nil {
log.Printf("failed to retrieve %s: %s\n", r.site, r.err)
continue
}
log.Printf("Retrieved site %s in %s\n", r.site, r.duration)
}
}()
// wait for program to finish
<-ctx.Done()
if err := ctx.Err(); err != context.Canceled {
fmt.Printf("\nprogram timed out: %s\n", err)
return
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(ctx context.Context, sites chan string, responses chan resp) {
// range through the receiving channel. When this channel is closed
// all go routines will terminate
for s := range sites {
// create a channel to to receive the response on locally
// make sure it's buffered so you don't leak a goroutine
work := make(chan resp, 1)
// pick a magic timeout duration
td := 2 * time.Second
// create a timeout.
ctx, cancel := context.WithTimeout(ctx, td)
defer cancel()
now := time.Now()
// retrieve the site in a goroutine to avoid blocking
go func() {
// retrieve the website
_, err := http.Get(s)
work <- resp{site: s, duration: time.Since(now), err: err}
}()
select {
case r := <-work:
// retrieve the value from our local function and pass it through to our send channel
responses <- r
case <-ctx.Done():
responses <- resp{site: s, err: fmt.Errorf("timed out after %s", td)}
}
}
}
// section: code
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
// create a custom type to receive our responses
type resp struct {
site string
duration time.Duration
err error
}
func main() {
// Load sites from a file
args := os.Args[1:]
// Args might be a single string with line returns to delimit sites, break them apart
if len(args) == 1 {
args = strings.Split(args[0], "\n")
}
// Create a channel to send all sites to
// We'll create it buffered so that we throttle how much many go routines will be processing at any given time
sites := make(chan string, 30)
// create an unbuffered channel to receive on
// because we don't have contention or latency on the end of our
// process, there is no need to buffer this channel
responses := make(chan resp)
now := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// launch the same number of go routines that we have a buffer size for
for i := 0; i < cap(sites); i++ {
go get(ctx, sites, responses)
}
// feed all of our data into the send channel
// this channel will block when it's full, and unblock
// when space becomes available
// We will do this in a goroutine so we don't block execution
go func() {
for _, s := range args {
sites <- s
}
// ensure that any open go routines close down
close(sites)
}()
// launch the reads in a goroutine to not block context cancellation
go func() {
// signal program has finished
defer cancel()
// read back our responses
for i := 0; i < len(args); i++ {
r := <-responses
if r.err != nil {
log.Printf("failed to retrieve %s: %s\n", r.site, r.err)
continue
}
log.Printf("Retrieved site %s in %s\n", r.site, r.duration)
}
}()
// wait for program to finish
<-ctx.Done()
if err := ctx.Err(); err != context.Canceled {
fmt.Printf("\nprogram timed out: %s\n", err)
return
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(ctx context.Context, sites chan string, responses chan resp) {
// range through the receiving channel. When this channel is closed
// all go routines will terminate
for s := range sites {
// create a channel to to receive the response on locally
// make sure it's buffered so you don't leak a goroutine
work := make(chan resp, 1)
// pick a magic timeout duration
td := 2 * time.Second
// create a timeout.
ctx, cancel := context.WithTimeout(ctx, td)
defer cancel()
now := time.Now()
// retrieve the site in a goroutine to avoid blocking
go func() {
// retrieve the website
_, err := http.Get(s)
work <- resp{site: s, duration: time.Since(now), err: err}
}()
select {
case r := <-work:
// retrieve the value from our local function and pass it through to our send channel
responses <- r
case <-ctx.Done():
responses <- resp{site: s, err: fmt.Errorf("timed out after %s", td)}
}
}
}
// section: codeWithoutCancel
Sometimes you need to perform cleanup operations that must complete even after the parent context is canceled. Common examples include:
- Logging - Recording metrics or errors after a request is canceled
- Metrics collection - Sending telemetry data regardless of request outcome
- Cleanup operations - Releasing resources or notifying external systems
context.WithoutCancel returns a copy of the parent context that is never canceled when the parent is canceled. The returned context has no deadline and returns nil for Err() and Done().
Basic Usage
// Parent context gets canceled
ctx, cancel := context.WithCancel(context.Background())
cancel()
// Create a context that won't be canceled
cleanupCtx := context.WithoutCancel(ctx)
// cleanupCtx.Done() returns nil - it will never be canceled
// cleanupCtx.Err() returns nil - no error
Practical Example: Metrics After Cancellation
Your context often carries request-scoped values like request IDs and trace IDs that your metrics system needs. When the request is canceled, you still want to send metrics with those values. WithoutCancel preserves the values while preventing the canceled context from aborting your metrics call.
func handleRequest(ctx context.Context, metricsURL string) {
defer sendMetrics(ctx, metricsURL)
reqID := ctx.Value(requestIDKey).(string)
fmt.Printf("[%s] processing request...\n", reqID)
select {
case <-time.After(500 * time.Millisecond):
fmt.Printf("[%s] request completed\n", reqID)
case <-ctx.Done():
fmt.Printf("[%s] request canceled: %v\n", reqID, ctx.Err())
}
}
func sendMetrics(ctx context.Context, metricsURL string) {
// WithoutCancel preserves values but strips both
// cancellation and the parent's deadline. Add a
// timeout so the HTTP call below won't hang forever.
logCtx := context.WithoutCancel(ctx)
logCtx, cancel := context.WithTimeout(logCtx, 2*time.Second)
defer cancel()
reqID := logCtx.Value(requestIDKey).(string)
fmt.Printf("[%s] sending metrics...\n", reqID)
req, _ := http.NewRequestWithContext(logCtx, "POST", metricsURL, nil)
req.Header.Set("X-Request-ID", reqID)
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("[%s] metrics failed: %v\n", reqID, err)
return
}
resp.Body.Close()
fmt.Printf("[%s] metrics sent (status %d)\n", reqID, resp.StatusCode)
}
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
)
type ctxKey string
const requestIDKey ctxKey = "request_id"
// section: log-metrics
func handleRequest(ctx context.Context, metricsURL string) {
defer sendMetrics(ctx, metricsURL)
reqID := ctx.Value(requestIDKey).(string)
fmt.Printf("[%s] processing request...\n", reqID)
select {
case <-time.After(500 * time.Millisecond):
fmt.Printf("[%s] request completed\n", reqID)
case <-ctx.Done():
fmt.Printf("[%s] request canceled: %v\n", reqID, ctx.Err())
}
}
func sendMetrics(ctx context.Context, metricsURL string) {
// WithoutCancel preserves values but strips both
// cancellation and the parent's deadline. Add a
// timeout so the HTTP call below won't hang forever.
logCtx := context.WithoutCancel(ctx)
logCtx, cancel := context.WithTimeout(logCtx, 2*time.Second)
defer cancel()
reqID := logCtx.Value(requestIDKey).(string)
fmt.Printf("[%s] sending metrics...\n", reqID)
req, _ := http.NewRequestWithContext(logCtx, "POST", metricsURL, nil)
req.Header.Set("X-Request-ID", reqID)
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("[%s] metrics failed: %v\n", reqID, err)
return
}
resp.Body.Close()
fmt.Printf("[%s] metrics sent (status %d)\n", reqID, resp.StatusCode)
}
// section: log-metrics
func main() {
// Fake metrics server for the demo
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("[server] received metrics from %s\n", r.Header.Get("X-Request-ID"))
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, requestIDKey, "req-abc-123")
go func() {
time.Sleep(100 * time.Millisecond)
cancel()
}()
handleRequest(ctx, srv.URL)
}
// section: output
/*
[req-abc-123] processing request...
[req-abc-123] request canceled: context canceled
[req-abc-123] sending metrics...
[server] received metrics from req-abc-123
[req-abc-123] metrics sent (status 200)
*/
// section: outputOutput:
/*
[req-abc-123] processing request...
[req-abc-123] request canceled: context canceled
[req-abc-123] sending metrics...
[server] received metrics from req-abc-123
[req-abc-123] metrics sent (status 200)
*/package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
)
type ctxKey string
const requestIDKey ctxKey = "request_id"
// section: log-metrics
func handleRequest(ctx context.Context, metricsURL string) {
defer sendMetrics(ctx, metricsURL)
reqID := ctx.Value(requestIDKey).(string)
fmt.Printf("[%s] processing request...\n", reqID)
select {
case <-time.After(500 * time.Millisecond):
fmt.Printf("[%s] request completed\n", reqID)
case <-ctx.Done():
fmt.Printf("[%s] request canceled: %v\n", reqID, ctx.Err())
}
}
func sendMetrics(ctx context.Context, metricsURL string) {
// WithoutCancel preserves values but strips both
// cancellation and the parent's deadline. Add a
// timeout so the HTTP call below won't hang forever.
logCtx := context.WithoutCancel(ctx)
logCtx, cancel := context.WithTimeout(logCtx, 2*time.Second)
defer cancel()
reqID := logCtx.Value(requestIDKey).(string)
fmt.Printf("[%s] sending metrics...\n", reqID)
req, _ := http.NewRequestWithContext(logCtx, "POST", metricsURL, nil)
req.Header.Set("X-Request-ID", reqID)
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("[%s] metrics failed: %v\n", reqID, err)
return
}
resp.Body.Close()
fmt.Printf("[%s] metrics sent (status %d)\n", reqID, resp.StatusCode)
}
// section: log-metrics
func main() {
// Fake metrics server for the demo
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("[server] received metrics from %s\n", r.Header.Get("X-Request-ID"))
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, requestIDKey, "req-abc-123")
go func() {
time.Sleep(100 * time.Millisecond)
cancel()
}()
handleRequest(ctx, srv.URL)
}
// section: output
/*
[req-abc-123] processing request...
[req-abc-123] request canceled: context canceled
[req-abc-123] sending metrics...
[server] received metrics from req-abc-123
[req-abc-123] metrics sent (status 200)
*/
// section: outputWithoutCancel strips both cancellation and the parent’s deadline. That means any downstream call using this context (HTTP, database, gRPC) would have no time limit at all. sendMetrics adds its own timeout so those calls won’t hang forever if the metrics service is unreachable.
Important Caveats
1. Add a timeout if you propagate the context downstream:
WithoutCancel strips both cancellation and the parent’s deadline. If you pass the context to a downstream call (HTTP, database, gRPC), that call will have no time limit. Add your own timeout so it can’t hang forever.
cleanupCtx := context.WithoutCancel(ctx)
cleanupCtx, cancel := context.WithTimeout(cleanupCtx, 5*time.Second)
defer cancel()
If you’re only reading values from the context and not passing it to another call, the timeout doesn’t matter.
2. Use sparingly:
WithoutCancel is for exceptional cases like cleanup, metrics, and logging. Normal application code should respect parent context cancellation.
AfterFunc
context.AfterFunc registers a callback to run in its own goroutine when a context is canceled. It returns a stop function that can prevent the callback from running if called before cancellation.
The problem it solves: Before AfterFunc, reacting to context cancellation during a blocking operation required spawning a goroutine and allocating channels on every call, even though cancellation almost never happens. AfterFunc only spawns a goroutine when cancellation actually fires.
Common usage patterns in production Go code:
- Cancelling blocking I/O — Go’s own
netpackage usesAfterFuncinternally to interrupt TCP dials by setting a write deadline when the context is canceled.crypto/tlsuses it to close connections during a canceled TLS handshake. - Context merging — Cancel when either of two independent contexts cancels. A server handler gets a request context but also needs to cancel when the server shuts down. Projects like Milvus, Temporal, and StackRox use this pattern.
- Resource cleanup — “When this context dies, close this resource.” Used by quic-go, Tailscale, and Docker for closing connections, loggers, and subscriptions tied to a context’s lifetime.
- Waking sync.Cond — Call
cond.Broadcast()when the context is canceled to unblock goroutines waiting on a condition variable. Used by Cilium’s promise implementation.
AfterFunc is a building block for systems and infrastructure code. For typical application code, listening on ctx.Done() in a select statement is simpler and more readable.
For details and examples, see the context package documentation.
Exercise (20m)
The following code retrieves book information from an open API. It currently uses timers to control timeout.
Rewrite the code using contexts to observe the same timeouts.
You can download the source code using the button in the left navigation.
main.go:
package main
import (
"fmt"
"log"
"time"
"training/api"
)
func main() {
isbns := []string{
"1788626540",
"178712598X",
"1838643575",
"0672338033",
"1788475275",
"1788622596",
"1788294289",
"1788627911",
"1838554491",
}
timer := time.NewTimer(time.Second * 30)
defer timer.Stop()
results := api.Books(isbns)
for i := 0; i < len(isbns); i++ {
select {
case result := <-results:
if result.Err != nil {
log.Fatal(result.Err)
}
fmt.Println(result.Book.Title)
case <-timer.C:
log.Fatal("timeout exceeded")
}
}
}package main
import (
"fmt"
"log"
"time"
"training/api"
)
func main() {
isbns := []string{
"1788626540",
"178712598X",
"1838643575",
"0672338033",
"1788475275",
"1788622596",
"1788294289",
"1788627911",
"1838554491",
}
timer := time.NewTimer(time.Second * 30)
defer timer.Stop()
results := api.Books(isbns)
for i := 0; i < len(isbns); i++ {
select {
case result := <-results:
if result.Err != nil {
log.Fatal(result.Err)
}
fmt.Println(result.Book.Title)
case <-timer.C:
log.Fatal("timeout exceeded")
}
}
}api/api.go
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
type BookResult struct {
Book Book
Err error
}
// Books will return a list of books based on the provided ISBN numbers
func Books(isbns []string) <-chan BookResult {
br := make(chan BookResult, len(isbns))
for _, isbn := range isbns {
// fan out go routines so they all request concurrently
go func(isbn string) {
// set a timeout for our book retrievals
timer := time.NewTimer(time.Second * 10)
defer timer.Stop()
work := make(chan BookResult, 1)
// don't block while retrieving the book
go func() {
book, err := getBook(isbn)
work <- BookResult{Book: book, Err: err}
}()
select {
case result := <-work:
br <- result
case <-timer.C:
br <- BookResult{Err: fmt.Errorf("timed out retrieving %q", isbn)}
}
}(isbn)
}
return br
}
// getBook retrieves the book requested via the isbn
func getBook(isbn string) (Book, error) {
u, err := url.Parse("https://openlibrary.org/isbn/" + isbn + ".json")
if err != nil {
return Book{}, err
}
resp, err := http.Get(u.String())
if err != nil {
return Book{}, err
}
defer resp.Body.Close()
var book Book
if err := json.NewDecoder(resp.Body).Decode(&book); err != nil {
return Book{}, err
}
return book, nil
}package api
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
type BookResult struct {
Book Book
Err error
}
// Books will return a list of books based on the provided ISBN numbers
func Books(isbns []string) <-chan BookResult {
br := make(chan BookResult, len(isbns))
for _, isbn := range isbns {
// fan out go routines so they all request concurrently
go func(isbn string) {
// set a timeout for our book retrievals
timer := time.NewTimer(time.Second * 10)
defer timer.Stop()
work := make(chan BookResult, 1)
// don't block while retrieving the book
go func() {
book, err := getBook(isbn)
work <- BookResult{Book: book, Err: err}
}()
select {
case result := <-work:
br <- result
case <-timer.C:
br <- BookResult{Err: fmt.Errorf("timed out retrieving %q", isbn)}
}
}(isbn)
}
return br
}
// getBook retrieves the book requested via the isbn
func getBook(isbn string) (Book, error) {
u, err := url.Parse("https://openlibrary.org/isbn/" + isbn + ".json")
if err != nil {
return Book{}, err
}
resp, err := http.Get(u.String())
if err != nil {
return Book{}, err
}
defer resp.Body.Close()
var book Book
if err := json.NewDecoder(resp.Body).Decode(&book); err != nil {
return Book{}, err
}
return book, nil
}api/book.go
package api
type Book struct {
Publishers []string `json:"publishers"`
NumberOfPages int `json:"number_of_pages"`
Isbn10 []string `json:"isbn_10"`
Covers []int `json:"covers"`
LastModified LastModified `json:"last_modified"`
LatestRevision int `json:"latest_revision"`
Key string `json:"key"`
Authors []Authors `json:"authors"`
Ocaid string `json:"ocaid"`
Contributions []string `json:"contributions"`
Languages []Languages `json:"languages"`
Classifications Classifications `json:"classifications"`
SourceRecords []string `json:"source_records"`
Title string `json:"title"`
Identifiers Identifiers `json:"identifiers"`
Created Created `json:"created"`
Isbn13 []string `json:"isbn_13"`
LocalID []string `json:"local_id"`
PublishDate string `json:"publish_date"`
Works []Works `json:"works"`
Type Type `json:"type"`
FirstSentence FirstSentence `json:"first_sentence"`
Revision int `json:"revision"`
}
type LastModified struct {
Type string `json:"type"`
Value string `json:"value"`
}
type Authors struct {
Key string `json:"key"`
}
type Languages struct {
Key string `json:"key"`
}
type Classifications struct {
}
type Identifiers struct {
Goodreads []string `json:"goodreads"`
Librarything []string `json:"librarything"`
}
type Created struct {
Type string `json:"type"`
Value string `json:"value"`
}
type Works struct {
Key string `json:"key"`
}
type Type struct {
Key string `json:"key"`
}
type FirstSentence struct {
Type string `json:"type"`
Value string `json:"value"`
}package api
type Book struct {
Publishers []string `json:"publishers"`
NumberOfPages int `json:"number_of_pages"`
Isbn10 []string `json:"isbn_10"`
Covers []int `json:"covers"`
LastModified LastModified `json:"last_modified"`
LatestRevision int `json:"latest_revision"`
Key string `json:"key"`
Authors []Authors `json:"authors"`
Ocaid string `json:"ocaid"`
Contributions []string `json:"contributions"`
Languages []Languages `json:"languages"`
Classifications Classifications `json:"classifications"`
SourceRecords []string `json:"source_records"`
Title string `json:"title"`
Identifiers Identifiers `json:"identifiers"`
Created Created `json:"created"`
Isbn13 []string `json:"isbn_13"`
LocalID []string `json:"local_id"`
PublishDate string `json:"publish_date"`
Works []Works `json:"works"`
Type Type `json:"type"`
FirstSentence FirstSentence `json:"first_sentence"`
Revision int `json:"revision"`
}
type LastModified struct {
Type string `json:"type"`
Value string `json:"value"`
}
type Authors struct {
Key string `json:"key"`
}
type Languages struct {
Key string `json:"key"`
}
type Classifications struct {
}
type Identifiers struct {
Goodreads []string `json:"goodreads"`
Librarything []string `json:"librarything"`
}
type Created struct {
Type string `json:"type"`
Value string `json:"value"`
}
type Works struct {
Key string `json:"key"`
}
type Type struct {
Key string `json:"key"`
}
type FirstSentence struct {
Type string `json:"type"`
Value string `json:"value"`
}Solution
While the code didn’t change a lot, we now have future capabilities to control other timeouts/cancellation, as well as add context specific values (we’ll cover this next).
main.go:
package main
import (
"context"
"fmt"
"log"
"time"
"training/api"
)
func main() {
isbns := []string{
"1788626540",
"178712598X",
"1838643575",
"0672338033",
"1788475275",
"1788622596",
"1788294289",
"1788627911",
"1838554491",
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
results := api.Books(ctx, isbns)
for i := 0; i < len(isbns); i++ {
select {
case result := <-results:
if result.Err != nil {
log.Fatal(result.Err)
}
fmt.Println(result.Book.Title)
case <-ctx.Done():
log.Fatal(ctx.Err())
}
}
}package main
import (
"context"
"fmt"
"log"
"time"
"training/api"
)
func main() {
isbns := []string{
"1788626540",
"178712598X",
"1838643575",
"0672338033",
"1788475275",
"1788622596",
"1788294289",
"1788627911",
"1838554491",
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
results := api.Books(ctx, isbns)
for i := 0; i < len(isbns); i++ {
select {
case result := <-results:
if result.Err != nil {
log.Fatal(result.Err)
}
fmt.Println(result.Book.Title)
case <-ctx.Done():
log.Fatal(ctx.Err())
}
}
}api/api.go
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
type BookResult struct {
Book Book
Err error
}
// Books will return a list of books based on the provided ISBN numbers
func Books(ctx context.Context, isbns []string) <-chan BookResult {
br := make(chan BookResult, len(isbns))
for _, isbn := range isbns {
// fan out go routines so they all request concurrently
go func(isbn string) {
// set a timeout for our book retrievals
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
work := make(chan BookResult, 1)
// don't block while retrieving the book
go func() {
book, err := getBook(isbn)
work <- BookResult{Book: book, Err: err}
}()
select {
case result := <-work:
br <- result
case <-ctx.Done():
br <- BookResult{Err: fmt.Errorf("timed out retrieving %q: %w", isbn, ctx.Err())}
}
}(isbn)
}
return br
}
// getBook retrieves the book requested via the isbn
func getBook(isbn string) (Book, error) {
u, err := url.Parse("https://openlibrary.org/isbn/" + isbn + ".json")
if err != nil {
return Book{}, err
}
resp, err := http.Get(u.String())
if err != nil {
return Book{}, err
}
defer resp.Body.Close()
var book Book
if err := json.NewDecoder(resp.Body).Decode(&book); err != nil {
return Book{}, err
}
return book, nil
}package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
type BookResult struct {
Book Book
Err error
}
// Books will return a list of books based on the provided ISBN numbers
func Books(ctx context.Context, isbns []string) <-chan BookResult {
br := make(chan BookResult, len(isbns))
for _, isbn := range isbns {
// fan out go routines so they all request concurrently
go func(isbn string) {
// set a timeout for our book retrievals
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
work := make(chan BookResult, 1)
// don't block while retrieving the book
go func() {
book, err := getBook(isbn)
work <- BookResult{Book: book, Err: err}
}()
select {
case result := <-work:
br <- result
case <-ctx.Done():
br <- BookResult{Err: fmt.Errorf("timed out retrieving %q: %w", isbn, ctx.Err())}
}
}(isbn)
}
return br
}
// getBook retrieves the book requested via the isbn
func getBook(isbn string) (Book, error) {
u, err := url.Parse("https://openlibrary.org/isbn/" + isbn + ".json")
if err != nil {
return Book{}, err
}
resp, err := http.Get(u.String())
if err != nil {
return Book{}, err
}
defer resp.Body.Close()
var book Book
if err := json.NewDecoder(resp.Body).Decode(&book); err != nil {
return Book{}, err
}
return book, nil
}Context Values
Contexts also allow us to pass values through our applications easily, using context.WithValue. This can clean up an application and can help make sure that necessary information is passed along between concurrent processes.
What Values Should Be Passed?
Do pass anything that is “request” specific, such as the “current user” or a request ID.
Don’t pass “global” values such as database connections or loggers.
Context Value Anti-Patterns
While context values are powerful, they can be misused. Here are critical anti-patterns to avoid:
❌ The “Side Car API” Anti-Pattern
Don’t use context as a side channel to pass function parameters. Context values should not replace explicit function parameters.
Bad Example (Side Car API):
// ❌ Bad: Using context to avoid explicit parameters
func ProcessOrder(ctx context.Context) error {
orderID := ctx.Value("orderID").(string) // Hidden dependency
userID := ctx.Value("userID").(int) // Hidden dependency
warehouse := ctx.Value("warehouse").(string) // Hidden dependency
// ... process order
}
// Caller has to stuff everything into context
ctx = context.WithValue(ctx, "orderID", "12345")
ctx = context.WithValue(ctx, "userID", 42)
ctx = context.WithValue(ctx, "warehouse", "NYC")
ProcessOrder(ctx)
Good Example (Explicit Parameters):
// ✓ Good: Function signature is clear and self-documenting
func ProcessOrder(ctx context.Context, orderID string, userID int, warehouse string) error {
// ... process order
}
// Caller's intent is clear
ProcessOrder(ctx, "12345", 42, "NYC")
Why this matters:
- API Clarity: Function signatures should document what the function needs
- Type Safety: Context values lose type information and require assertions
- Discoverability: IDEs can’t autocomplete or validate context values
- Testing: Explicit parameters are easier to mock and test
- Refactoring: Changes to context values can break code silently
Context Values: The Right Use Cases
✓ Use context values for request-scoped data that crosses API boundaries:
- Trace IDs - For distributed tracing across services
- Request IDs - For logging and correlation
- Authentication tokens - User identity across middleware layers
- Deadline/timeout information - Already handled by context itself
Example of appropriate use:
// ✓ Good: Authentication middleware adds user to context
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := authenticateRequest(r)
ctx := WithUser(r.Context(), user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// ✓ Good: Handler extracts user from context
func OrderHandler(w http.ResponseWriter, r *http.Request) {
user, _ := UserFromContext(r.Context())
// orderID comes from explicit parameter (URL path or body)
// user comes from context (set by middleware)
ProcessOrder(r.Context(), orderID, user.ID)
}
Rule of thumb: If a caller would normally pass it as a parameter, make it a parameter. Only use context for cross-cutting concerns that permeate many layers.
Using WithValue
You can use the WithValue function to add any value to a context via a specific key.
package main
import (
"context"
"fmt"
)
func main() {
// Add the value of "42" with the key of "id"
// section: set
ctx := context.WithValue(context.Background(), "id", 42)
// section: set
// Retrieve the value back out
// section: retrieve
id, ok := ctx.Value("id").(int)
if !ok {
fmt.Println("id not fount")
return
}
fmt.Printf("id is %d\n", id)
// section: retrieve
}The context now has a key called id with the value of 42 set.
You can retrieve a value from a context using the Value method:
id, ok := ctx.Value("id").(int)
if !ok {
fmt.Println("id not fount")
return
}
fmt.Printf("id is %d\n", id)package main
import (
"context"
"fmt"
)
func main() {
// Add the value of "42" with the key of "id"
// section: set
ctx := context.WithValue(context.Background(), "id", 42)
// section: set
// Retrieve the value back out
// section: retrieve
id, ok := ctx.Value("id").(int)
if !ok {
fmt.Println("id not fount")
return
}
fmt.Printf("id is %d\n", id)
// section: retrieve
}It’s important to note that all values are stored as an interface in context. This means that you need to do a type assertion when retrieving them to restore the proper type to the variable.
Interface Values
As we stated in the previous example, all values are stored as an empty interface{} (any) in the context tree.
The following example shows the danger in skipping the type assertion when retrieving values from a context:
func main() {
// Add the value of "42" with the key of "id"
ctx := context.WithValue(context.Background(), "id", 42)
// Retrieve the value back out
id := ctx.Value("id")
fmt.Printf("id is %d, %T\n", id, id)
fmt.Println(id + 10)
}
package main
import (
"context"
"fmt"
)
// section: code
func main() {
// Add the value of "42" with the key of "id"
ctx := context.WithValue(context.Background(), "id", 42)
// Retrieve the value back out
id := ctx.Value("id")
fmt.Printf("id is %d, %T\n", id, id)
fmt.Println(id + 10)
}
// section: code
/*
// section: output
./main.go:17:17: invalid operation: id + 10 (mismatched types interface {} and int)
// section: output
*/This will result in a runtime panic of type mismatch:
package main
import (
"context"
"fmt"
)
// section: code
func main() {
// Add the value of "42" with the key of "id"
ctx := context.WithValue(context.Background(), "id", 42)
// Retrieve the value back out
id := ctx.Value("id")
fmt.Printf("id is %d, %T\n", id, id)
fmt.Println(id + 10)
}
// section: code
/*
// section: output
./main.go:17:17: invalid operation: id + 10 (mismatched types interface {} and int)
// section: output
*/Non-string Context Keys
Context keys must be comparable, but using strings like the previous example isn’t a good idea. String keys aren’t namespaced, so if two packages use the same string key, they will collide and likely overwrite or retrieve the wrong data.
For instance, suppose that we different packages that we use that we pass our context through. If those packages use the same key name, such as ID, they will overwrite that key if any other package also uses that same value for the key identifier.
In concurrent programs, it is very common to pass your context around through many packages. If we think about a RESTful API built in Go, the context would get passed to the server, then to the handlers, then to the database, and likely through a handful of middleware on both the web and database layers. In addition, if it also uses any type of metrics package to track the distributed system performance, it is likely to have dozens of keys stored in the context for each specific layer/system.
One way to ensure that you don’t collide with the same key name another package uses is to use a very obscure key name, like this-is-my-key-nobody-else-use-this-please. As I suspect you may already agree, that isn’t a great approach.
Overlapping Middleware
Given a middleware package that uses an “id” key to set a current user:
package middleware
import (
"context"
"fmt"
)
// section: authenticate
func Authenticate(ctx context.Context) context.Context {
return context.WithValue(ctx, "id", 42)
}
// section: authenticate
func PrintUserID(ctx context.Context) {
userID := ctx.Value("id")
fmt.Println("User ID:", userID)
}And an orders package that uses an “id” key to track the next order id to create:
func NextOrderID(ctx context.Context) context.Context {
defer func() { nextID++ }()
return context.WithValue(ctx, "id", nextID)
}
package orders
import (
"context"
"fmt"
)
var nextID int
// section: order-id
func NextOrderID(ctx context.Context) context.Context {
defer func() { nextID++ }()
return context.WithValue(ctx, "id", nextID)
}
// section: order-id
func CreateOrder(ctx context.Context) {
id := ctx.Value("id").(int)
fmt.Println("Order ID:", id)
}Using them together will cause interference:
func main() {
ctx := context.Background()
ctx = middleware.Authenticate(ctx)
middleware.PrintUserID(ctx)
ctx = orders.NextOrderID(ctx)
orders.CreateOrder(ctx)
middleware.PrintUserID(ctx)
}
package main
import (
"context"
"training/middleware"
"training/orders"
)
// section: main
func main() {
ctx := context.Background()
ctx = middleware.Authenticate(ctx)
middleware.PrintUserID(ctx)
ctx = orders.NextOrderID(ctx)
orders.CreateOrder(ctx)
middleware.PrintUserID(ctx)
}
// section: mainUser ID: 42
Order ID: 0
User ID: 0
Namespacing Context Values
A common practice is to use an unexported (private) type that only your package has access to. Because the Context looks up values based on both the type and value of the key, only your package will have access to that specific key.
The first step is to declare your unexported type:
package main
import (
"context"
"errors"
"fmt"
)
// section: type
type key int
// section: type
// section: const
const userKey key = 0
// section: const
type User struct {
ID int
Name string
}
// section: with-user
// WithUser will embed the User in the provided context
func WithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
// section: with-user
// section: from-context
// FromContext will retrieve the User from the provided context
func FromContext(ctx context.Context) (*User, error) {
u, ok := ctx.Value(userKey).(*User)
if !ok {
return nil, errors.New("user not found")
}
return u, nil
}
// section: from-context
// section: main
func main() {
u := &User{
ID: 0,
Name: "Tim",
}
ctx := WithUser(context.Background(), u)
printUser(ctx)
}
func printUser(ctx context.Context) {
u, err := FromContext(ctx)
if err != nil {
fmt.Println("err getting user:", err)
return
}
fmt.Println("user:", u)
}
// section: mainThen, using the type you just defined, declare a const to use as a key:
package main
import (
"context"
"errors"
"fmt"
)
// section: type
type key int
// section: type
// section: const
const userKey key = 0
// section: const
type User struct {
ID int
Name string
}
// section: with-user
// WithUser will embed the User in the provided context
func WithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
// section: with-user
// section: from-context
// FromContext will retrieve the User from the provided context
func FromContext(ctx context.Context) (*User, error) {
u, ok := ctx.Value(userKey).(*User)
if !ok {
return nil, errors.New("user not found")
}
return u, nil
}
// section: from-context
// section: main
func main() {
u := &User{
ID: 0,
Name: "Tim",
}
ctx := WithUser(context.Background(), u)
printUser(ctx)
}
func printUser(ctx context.Context) {
u, err := FromContext(ctx)
if err != nil {
fmt.Println("err getting user:", err)
return
}
fmt.Println("user:", u)
}
// section: mainThe value of the key doesn’t matter. Using a new unexported type is what prevents keys from becoming comparable even if another package uses the value of 0 as a key somewhere else.
If a package uses multiple key-values pairs, each const should have a unique value. For instance, if we also needed a request ID, we would declare it as well in our package:
const userKey key = 0
const requestKey key = 1
Context Getters / Setters
Because you’ll always use the same keys to access the values in the context, it’s also common to create your own setters and getters. This removes the need for you to keep track of which keys are used, and allow for you to call methods for easier readability/usage.
Setter:
// WithUser will embed the User in the provided context
func WithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
package main
import (
"context"
"errors"
"fmt"
)
// section: type
type key int
// section: type
// section: const
const userKey key = 0
// section: const
type User struct {
ID int
Name string
}
// section: with-user
// WithUser will embed the User in the provided context
func WithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
// section: with-user
// section: from-context
// FromContext will retrieve the User from the provided context
func FromContext(ctx context.Context) (*User, error) {
u, ok := ctx.Value(userKey).(*User)
if !ok {
return nil, errors.New("user not found")
}
return u, nil
}
// section: from-context
// section: main
func main() {
u := &User{
ID: 0,
Name: "Tim",
}
ctx := WithUser(context.Background(), u)
printUser(ctx)
}
func printUser(ctx context.Context) {
u, err := FromContext(ctx)
if err != nil {
fmt.Println("err getting user:", err)
return
}
fmt.Println("user:", u)
}
// section: mainGetter:
// FromContext will retrieve the User from the provided context
func FromContext(ctx context.Context) (*User, error) {
u, ok := ctx.Value(userKey).(*User)
if !ok {
return nil, errors.New("user not found")
}
return u, nil
}
package main
import (
"context"
"errors"
"fmt"
)
// section: type
type key int
// section: type
// section: const
const userKey key = 0
// section: const
type User struct {
ID int
Name string
}
// section: with-user
// WithUser will embed the User in the provided context
func WithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
// section: with-user
// section: from-context
// FromContext will retrieve the User from the provided context
func FromContext(ctx context.Context) (*User, error) {
u, ok := ctx.Value(userKey).(*User)
if !ok {
return nil, errors.New("user not found")
}
return u, nil
}
// section: from-context
// section: main
func main() {
u := &User{
ID: 0,
Name: "Tim",
}
ctx := WithUser(context.Background(), u)
printUser(ctx)
}
func printUser(ctx context.Context) {
u, err := FromContext(ctx)
if err != nil {
fmt.Println("err getting user:", err)
return
}
fmt.Println("user:", u)
}
// section: mainUsing Getters & Setters
We can then use these getters and setters as needed in our code:
func main() {
u := &User{
ID: 0,
Name: "Tim",
}
ctx := WithUser(context.Background(), u)
printUser(ctx)
}
func printUser(ctx context.Context) {
u, err := FromContext(ctx)
if err != nil {
fmt.Println("err getting user:", err)
return
}
fmt.Println("user:", u)
}
package main
import (
"context"
"errors"
"fmt"
)
// section: type
type key int
// section: type
// section: const
const userKey key = 0
// section: const
type User struct {
ID int
Name string
}
// section: with-user
// WithUser will embed the User in the provided context
func WithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
// section: with-user
// section: from-context
// FromContext will retrieve the User from the provided context
func FromContext(ctx context.Context) (*User, error) {
u, ok := ctx.Value(userKey).(*User)
if !ok {
return nil, errors.New("user not found")
}
return u, nil
}
// section: from-context
// section: main
func main() {
u := &User{
ID: 0,
Name: "Tim",
}
ctx := WithUser(context.Background(), u)
printUser(ctx)
}
func printUser(ctx context.Context) {
u, err := FromContext(ctx)
if err != nil {
fmt.Println("err getting user:", err)
return
}
fmt.Println("user:", u)
}
// section: mainExercise (15m)
The current code has inadvertently used the same key values with different packages. As a result, the tests are now failing.
To make the tests pass again, you’ll need to perform the following steps:
- Define unexported types for each package for your key types
- Declare new constants based on the new unexported types
- Update each corresponding setter and getter to use the new constants
- Run the tests to ensure the code is now working
To start the exercise, use the Download Source button in the left hand navigation. Then, open the folder exercises/02-middleware in your editor.
To run the tests, you can use the following command from the exercises/02-middleware:
go test ./...
Note: No changes are needed to make the tests pass. Only the domain and tracking packages need to be modified.
Here is the test:
func TestContext(t *testing.T) {
ctx := context.Background()
expUser := domain.User{ID: 10, Name: "Rob"}
ctx = domain.WithContext(ctx, &expUser)
expTracer := tracking.Tracer{ID: 20}
ctx = tracking.WithContext(ctx, &expTracer)
gotUser, err := domain.FromContext(ctx)
if err != nil {
t.Error(err)
}
if !cmp.Equal(gotUser, &expUser) {
t.Errorf("unexpected user\n%s\n", cmp.Diff(gotUser, expUser))
}
gotTracer, err := tracking.FromContext(ctx)
if err != nil {
t.Error(err)
}
if !cmp.Equal(gotTracer, &expTracer) {
t.Errorf("unexpected tracer\n%s\n", cmp.Diff(gotTracer, expTracer))
}
}
package main
import (
"context"
"testing"
"training/domain"
"training/tracking"
"github.com/google/go-cmp/cmp"
)
// section: test
func TestContext(t *testing.T) {
ctx := context.Background()
expUser := domain.User{ID: 10, Name: "Rob"}
ctx = domain.WithContext(ctx, &expUser)
expTracer := tracking.Tracer{ID: 20}
ctx = tracking.WithContext(ctx, &expTracer)
gotUser, err := domain.FromContext(ctx)
if err != nil {
t.Error(err)
}
if !cmp.Equal(gotUser, &expUser) {
t.Errorf("unexpected user\n%s\n", cmp.Diff(gotUser, expUser))
}
gotTracer, err := tracking.FromContext(ctx)
if err != nil {
t.Error(err)
}
if !cmp.Equal(gotTracer, &expTracer) {
t.Errorf("unexpected tracer\n%s\n", cmp.Diff(gotTracer, expTracer))
}
}
// section: test
/*
// section: output
$ go test ./...
--- FAIL: TestContext (0.00s)
main_test.go:23: user not found
main_test.go:26: unexpected user
interface{}(
- (*domain.User)(nil),
+ domain.User{ID: 10, Name: "Rob"},
)
FAIL
FAIL training 0.094s
? training/domain [no test files]
? training/tracking [no test files]
FAIL
// section: output
*/Here is the current failing output of the test:
$ go test ./...
--- FAIL: TestContext (0.00s)
main_test.go:23: user not found
main_test.go:26: unexpected user
interface{}(
- (*domain.User)(nil),
+ domain.User{ID: 10, Name: "Rob"},
)
FAIL
FAIL training 0.094s
? training/domain [no test files]
? training/tracking [no test files]
FAIL
package main
import (
"context"
"testing"
"training/domain"
"training/tracking"
"github.com/google/go-cmp/cmp"
)
// section: test
func TestContext(t *testing.T) {
ctx := context.Background()
expUser := domain.User{ID: 10, Name: "Rob"}
ctx = domain.WithContext(ctx, &expUser)
expTracer := tracking.Tracer{ID: 20}
ctx = tracking.WithContext(ctx, &expTracer)
gotUser, err := domain.FromContext(ctx)
if err != nil {
t.Error(err)
}
if !cmp.Equal(gotUser, &expUser) {
t.Errorf("unexpected user\n%s\n", cmp.Diff(gotUser, expUser))
}
gotTracer, err := tracking.FromContext(ctx)
if err != nil {
t.Error(err)
}
if !cmp.Equal(gotTracer, &expTracer) {
t.Errorf("unexpected tracer\n%s\n", cmp.Diff(gotTracer, expTracer))
}
}
// section: test
/*
// section: output
$ go test ./...
--- FAIL: TestContext (0.00s)
main_test.go:23: user not found
main_test.go:26: unexpected user
interface{}(
- (*domain.User)(nil),
+ domain.User{ID: 10, Name: "Rob"},
)
FAIL
FAIL training 0.094s
? training/domain [no test files]
? training/tracking [no test files]
FAIL
// section: output
*/Here are the related files (hint, these are the only two files that need to change):
domain/user.go:
package domain
import (
"context"
"errors"
)
type User struct {
ID int
Name string
}
func FromContext(ctx context.Context) (*User, error) {
u, ok := ctx.Value("ID").(*User)
if !ok {
return nil, errors.New("user not found")
}
return u, nil
}
func WithContext(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, "ID", u)
}package domain
import (
"context"
"errors"
)
type User struct {
ID int
Name string
}
func FromContext(ctx context.Context) (*User, error) {
u, ok := ctx.Value("ID").(*User)
if !ok {
return nil, errors.New("user not found")
}
return u, nil
}
func WithContext(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, "ID", u)
}tracking/tracking.go:
package tracking
import (
"context"
"errors"
)
type Tracer struct {
ID int
}
func WithContext(ctx context.Context, t *Tracer) context.Context {
return context.WithValue(ctx, "ID", t)
}
func FromContext(ctx context.Context) (*Tracer, error) {
t, ok := ctx.Value("ID").(*Tracer)
if !ok {
return nil, errors.New("tracer not found")
}
return t, nil
}package tracking
import (
"context"
"errors"
)
type Tracer struct {
ID int
}
func WithContext(ctx context.Context, t *Tracer) context.Context {
return context.WithValue(ctx, "ID", t)
}
func FromContext(ctx context.Context) (*Tracer, error) {
t, ok := ctx.Value("ID").(*Tracer)
if !ok {
return nil, errors.New("tracer not found")
}
return t, nil
}Solution
Here are the only two files that needed changes to work.
The fix was to add unexported types and declare the corresponding constants from those keys.
domain/user.go:
package domain
import (
"context"
"errors"
)
// Define our custom key type
type key int
// declare our key value
const userKey key = 0
type User struct {
ID int
Name string
}
func FromContext(ctx context.Context) (*User, error) {
u, ok := ctx.Value(userKey).(*User)
if !ok {
return nil, errors.New("user not found")
}
return u, nil
}
func WithContext(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}package domain
import (
"context"
"errors"
)
// Define our custom key type
type key int
// declare our key value
const userKey key = 0
type User struct {
ID int
Name string
}
func FromContext(ctx context.Context) (*User, error) {
u, ok := ctx.Value(userKey).(*User)
if !ok {
return nil, errors.New("user not found")
}
return u, nil
}
func WithContext(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}tracking/tracking.go:
package tracking
import (
"context"
"errors"
)
// Define our custom key type
type key int
// declare our key value
const tracerKey key = 0
type Tracer struct {
ID int
}
func WithContext(ctx context.Context, t *Tracer) context.Context {
return context.WithValue(ctx, tracerKey, t)
}
func FromContext(ctx context.Context) (*Tracer, error) {
t, ok := ctx.Value(tracerKey).(*Tracer)
if !ok {
return nil, errors.New("tracer not found")
}
return t, nil
}package tracking
import (
"context"
"errors"
)
// Define our custom key type
type key int
// declare our key value
const tracerKey key = 0
type Tracer struct {
ID int
}
func WithContext(ctx context.Context, t *Tracer) context.Context {
return context.WithValue(ctx, tracerKey, t)
}
func FromContext(ctx context.Context) (*Tracer, error) {
t, ok := ctx.Value(tracerKey).(*Tracer)
if !ok {
return nil, errors.New("tracer not found")
}
return t, nil
}If we run main.go we’ll also get the expected results:
package main
import (
"context"
"fmt"
"log"
"training/domain"
"training/tracking"
)
func main() {
ctx := context.Background()
ctx = setContext(ctx)
getContext(ctx)
}
func setContext(ctx context.Context) context.Context {
u := domain.User{ID: 10, Name: "Rob"}
ctx = domain.WithContext(ctx, &u)
tracer := tracking.Tracer{ID: 20}
ctx = tracking.WithContext(ctx, &tracer)
return ctx
}
func getContext(ctx context.Context) {
u, err := domain.FromContext(ctx)
if err != nil {
log.Println(err)
}
fmt.Println("user:", u)
t, err := tracking.FromContext(ctx)
if err != nil {
log.Println(err)
}
fmt.Println("tracer:", t)
}
/*
// section: output
user: &{10 Rob}
tracer: &{20}
// section: output
*/