# Go: Using Context for Request Cancellation and Timeout Management


In programming, _context_ refers to information about the environment in which an object exists or a function executes. In Go, context usually refers to the `Context` interface from the `context` package. It was originally designed to make working with HTTP requests easier. However, contexts can also be used in regular concurrent code. Let's see how exactly.

---

## Canceling with Channel

Suppose we have an `execute()` function, which can run a given function and supports cancellation:

```go
// execute runs fn in a separate goroutine
// and waits for the result unless canceled.
func execute(cancel <-chan struct{}, fn func() int) (int, error) {
    ch := make(chan int, 1)

    go func() {
        ch <- fn()
    }()

    select {
    case res := <-ch:
        return res, nil
    case <-cancel:
        return 0, errors.New("canceled")
    }
}
```

Everything is familiar here:

* The function takes a channel through which it can receive a cancellation signal.
* It runs `fn()` in a separate goroutine.
* It uses select to wait for `fn()` to complete or cancel, whichever occurs first.

Let's write a client that cancels operations with a 50% probability:

```go
// work does something for 100 ms.
func work() int {
    time.Sleep(100 * time.Millisecond)
    fmt.Println("work done")
    return 42
}

// maybeCancel waits for 50 ms and cancels with 50% probability.
func maybeCancel(cancel chan struct{}) {
    time.Sleep(50 * time.Millisecond)
    if rand.Float32() < 0.5 {
        close(cancel)
    }
}

func main() {
    cancel := make(chan struct{})
    go maybeCancel(cancel)

    res, err := execute(cancel, work)
    fmt.Println(res, err)
}
```

```
work done
42 <nil>
```

Run it a few times:

```
0 canceled
```

```
work done
42 <nil>
```

```
0 canceled
```

```
work done
42 <nil>
```

No surprises here.

Now let's reimplement `execute` with context.

---

## Canceling with Context

The main purpose of context in Go is to cancel operations.

Let's reimplement what we just did with a cancel channel – this time with a context. The `execute()` function accepts a context `ctx` instead of a `cancel` channel:

```go
// execute runs fn in a separate goroutine
// and waits for the result unless canceled.
func execute(ctx context.Context, fn func() int) (int, error) {
    ch := make(chan int, 1)

    go func() {
        ch <- fn()
    }()

    select {
    case res := <-ch:
        return res, nil
    case <-ctx.Done():       // (1)
        return 0, ctx.Err()  // (2)
    }
}
```

The code has barely changed:

* ➊ Instead of the `cancel` channel, the cancellation signal comes from the `ctx.Done()` channel
* ➋ Instead of manually creating a "canceled" error, we return `ctx.Err()`

The client also changes slightly:

```go
// maybeCancel waits for 50 ms and cancels with 50% probability.
func maybeCancel(cancel func()) {
    time.Sleep(50 * time.Millisecond)
    if rand.Float32() < 0.5 {
        cancel()
    }
}

func main() {
    ctx := context.Background()              // (1)
    ctx, cancel := context.WithCancel(ctx)   // (2)
    defer cancel()                           // (3)

    go maybeCancel(cancel)                   // (4)

    res, err := execute(ctx, work)           // (5)
    fmt.Println(res, err)
}
```

```
work done
42 <nil>
```

Here's what we do:

* ➊ Create an empty context with `context.Background()`.
* ➋ Create a manual cancel context based on the empty context with `context.WithCancel()`.
* ➌ Schedule a deferred cancel when `main()` exits.
* ➍ Cancel the context with a 50% probability.
* ➎ Pass the context to the `execute()` function.

`context.WithCancel()` returns the context itself and a `cancel` function to cancel it. Calling `cancel()` releases the resources occupied by the context and closes the `ctx.Done()` channel — we use this effect to interrupt `execute()`. If the context is canceled, `ctx.Err()` returns an error (in our case `context.Canceled`).

All in all, it works exactly the same as the previous version with the cancel channel:

```
work done
42 <nil>
```

```
0 context canceled
```

```
0 context canceled
```

```
work done
42 <nil>
```

A few nuances that were not present with the cancel channel:

**Context is layered**. A context object is immutable. To add new properties to a context, a new (child) context is created based on the old (parent) context. That's why we first created an empty context and then a cancel context based on it:

```go
// parent context
ctx := context.Background()

// child context
ctx, cancel := context.WithCancel(ctx)
```

If the parent context is canceled, all child contexts are canceled as well (but not vice versa):

```go
// parent context
parentCtx, parentCancel := context.WithCancel(context.Background())

// child context
childCtx, childCancel := context.WithCancel(parentCtx)

// parentCancel() cancels both parentCtx and childCtx.
// childCancel() cancels only childCtx.
```

**Multiple cancels are safe**. If you call `close()` on a channel twice, it will cause a panic. However, you can call `cancel()` on the context as many times as you want. The first cancel will work, and the rest will be ignored. This is convenient because you can schedule a deferred `cancel()` right after creating the context, and explicitly cancel the context if necessary (as we did in the `maybeCancel` function). This wouldn't be possible with a channel.

---

## Timeout

The real power of context is its ability to handle both manual cancellation and _timeouts_.

```go
// execute runs fn in a separate goroutine
// and waits for the result unless canceled.
func execute(ctx context.Context, fn func() int) (int, error) {
    // remains unchanged
}

// work does something for 100 ms.
func work() int {
    // remains unchanged
}
```

With a timeout of 150 ms, `work()` completes on time:

```go
func main() {
    timeout := 150 * time.Millisecond
    ctx, cancel := context.WithTimeout(context.Background(), timeout)  // (1)
    defer cancel()

    res, err := execute(ctx, work)
    fmt.Println(res, err)
}
```

```
work done
42 <nil>
```

With a timeout of 50 ms, execution gets canceled:

```go
func main() {
    timeout := 50 * time.Millisecond
    ctx, cancel := context.WithTimeout(context.Background(), timeout)  // (1)
    defer cancel()

    res, err := execute(ctx, work)
    fmt.Println(res, err)
}
```

```
0 context deadline exceeded
```

The `execute()` function remains unchanged, but `context.WithCancel()` in main is now replaced with `context.WithTimeout()` ➊. This change causes `execute()` to fail with a timeout error (`context.DeadlineExceeded`) when `work()` doesn't finish on time.

Thanks to the context, the `execute()` function doesn't need to know whether the cancellation was triggered manually or by a timeout. All it needs to do is listen for the cancellation signal on the `ctx.Done()` channel.

Convenient!

---

## Parent and Child Timeouts

Let's say we have the same `execute()` function and two functions it can run — the faster `work()` and the slower `slow()`:

```go
// work does something for 100 ms.
func work() int {
    time.Sleep(100 * time.Millisecond)
    return 42
}

// slow does something for 200 ms.
func slow() int {
    time.Sleep(200 * time.Millisecond)
    return 99
}
```

We want to run both functions, but we don't want to wait more than 150 ms total. We can create a parent context with a 150 ms timeout and then create child contexts for each function:

```go
func main() {
    parentCtx, parentCancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
    defer parentCancel()

    // Child context for work()
    workCtx, workCancel := context.WithCancel(parentCtx)
    defer workCancel()

    // Child context for slow()
    slowCtx, slowCancel := context.WithCancel(parentCtx)
    defer slowCancel()

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        res, err := execute(workCtx, work)
        fmt.Println("work:", res, err)
    }()

    go func() {
        defer wg.Done()
        res, err := execute(slowCtx, slow)
        fmt.Println("slow:", res, err)
    }()

    wg.Wait()
}
```

```
work: 42 <nil>
slow: 0 context deadline exceeded
```

Here, `work()` completes successfully, but `slow()` gets canceled because it exceeds the parent timeout of 150 ms.

---

## Deadline

Instead of specifying a timeout duration, you can set an absolute deadline:

```go
func main() {
    deadline := time.Now().Add(150 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    res, err := execute(ctx, work)
    fmt.Println(res, err)
}
```

```
work done
42 <nil>
```

`context.WithDeadline()` works similarly to `context.WithTimeout()`, but takes an absolute time instead of a duration.

---

## Cancellation Cause

In Go 1.21+, you can specify a custom cause for cancellation. The `context.WithCancelCause()` function takes an additional parameter: the root _cause_ of the cancellation.

```go
ctx, cancel := context.WithCancelCause(context.Background())
cancel(errors.New("the night is dark"))
```

Use `context.Cause()` to get the error's cause:

```go
fmt.Println(ctx.Err())
// context canceled

fmt.Println(context.Cause(ctx))
// the night is dark
```

```
context canceled
the night is dark
```

In Go 1.21+, you can specify a custom cause for timeout (or deadline) cancellation when creating a context. This cause is accessible through `context.Cause()` when the context is canceled due to a timeout (or deadline):

```go
cause := errors.New("the night is dark")
ctx, cancel := context.WithTimeoutCause(
    context.Background(), 10*time.Millisecond, cause,
)
defer cancel()

time.Sleep(50 * time.Millisecond)
fmt.Println(ctx.Err())
// context deadline exceeded
fmt.Println(context.Cause(ctx))
// the night is dark
```

```
context deadline exceeded
the night is dark
```

---

## context.AfterFunc

Suppose we're performing a long-running task with the option to cancel:

```go
// work does something for 100 ms.
func work(ctx context.Context) {
    select {
    case <-time.After(100 * time.Millisecond):
    case <-ctx.Done():
    }
}
```

Let's set the timeout to 50 ms (expecting `work()` to be canceled):

```go
// the context is canceled after a 50 ms timeout
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

start := time.Now()
work(ctx)

if ctx.Err() != nil {
    fmt.Println("canceled after", time.Since(start))
}
```

```
canceled after 50ms
```

What should we do if `work()` has occupied resources that need to be freed upon cancellation?

```go
// cleanup frees up occupied resources.
func cleanup() {
    fmt.Println("cleanup")
}
```

We can add the `cleanup()` call directly into `work()`:

```go
func work(ctx context.Context) {
    select {
    case <-time.After(100 * time.Millisecond):
    case <-ctx.Done():
        cleanup()
    }
}
```

But Go 1.21+ offers a more flexible solution.

### Calling a Function on Context Cancellation

You can register a function to execute when the context is canceled:

```go
// the context is canceled after a 50 ms timeout
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

// cleanup is called after the context is canceled
context.AfterFunc(ctx, cleanup)

start := time.Now()
work(ctx)

if ctx.Err() != nil {
    fmt.Println("canceled after", time.Since(start))
}
```

```
cleanup
canceled after 50ms
```

In this version, `work()` doesn't need to know about `cleanup()`.

`context.AfterFunc()` offers the following:

* `cleanup()` runs in a separate goroutine.
* You can register multiple functions by calling `AfterFunc()` multiple times. Each function runs independently in a separate goroutine when the context is canceled.
* You can change your mind and "detach" a registered function.

### Registering a Function After the Context is Canceled

If the context is already canceled when the function is registered, the function executes immediately:

```go
// the context is canceled after a 50 ms timeout
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

start := time.Now()
work(ctx)

if ctx.Err() != nil {
    fmt.Println("canceled after", time.Since(start))
}

// cleanup is called immediately since the context is already canceled
context.AfterFunc(ctx, cleanup)
```

```
canceled after 50ms
cleanup
```

### Canceling the Registration

Example of "changing one's mind":

```go
// the context is canceled after a 50 ms timeout
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

// cleanup is called after the context is canceled
stopCleanup := context.AfterFunc(ctx, cleanup)  // (1)

// I changed my mind, let's not call cleanup
stopped := stopCleanup()                        // (2)
work(ctx)

fmt.Println("stopped cleanup:", stopped)
```

```
stopped cleanup: true
```

In ➊, we saved the "detach" function (returned by `context.AfterFunc()`) in the `stopCleanup` variable, and in ➋, we called it, detaching `cleanup()` from `ctx`. As a result, the context was canceled due to the timeout, but `cleanup()` did not execute.

If the context is canceled and the function has already started executing, you can't detach it:

```go
// the context is canceled after a 50 ms timeout
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

// cleanup is called after the context is canceled
stopCleanup := context.AfterFunc(ctx, cleanup)

work(ctx)

// I changed my mind about calling cleanup, but it's already too late
stopped := stopCleanup()
fmt.Println("stopped cleanup:", stopped)
```

```
cleanup
stopped cleanup: false
```

Phew. `AfterFunc()` is not the most intuitive context-related feature.

---

## Context with Values

The main purpose of context in Go is to cancel operations, either manually or by timeout/deadline. But it can also pass additional information about a call using `context.WithValue()`, which creates a context with a value for a specific key:

```go
type contextKey string

// "id" and "user" keys
var idKey = contextKey("id")
var userKey = contextKey("user")

// work does something.
func work() int {
    return 42
}

func main() {
    {
        ctx := context.Background()
        // context with request ID
        ctx = context.WithValue(ctx, idKey, 1234)
        // and user
        ctx = context.WithValue(ctx, userKey, "admin")
        res := execute(ctx, work)
        fmt.Println(res)
    }

    {
        // empty context
        ctx := context.Background()
        res := execute(ctx, work)
        fmt.Println(res)
    }
}
```

We use values of a custom type `contextKey` as keys instead of strings or numbers. This prevents conflicts if two packages modify the same context and both add a value with the `id` or `user` key.

To retrieve a value by key, use the `Value()` method:

```go
// execute runs fn with respect to ctx.
func execute(ctx context.Context, fn func() int) int {
    reqId := ctx.Value(idKey)
    if reqId != nil {
        fmt.Printf("Request ID = %d\n", reqId)
    } else {
        fmt.Println("Request ID unknown")
    }

    user := ctx.Value(userKey)
    if user != nil {
        fmt.Printf("Request user = %s\n", user)
    } else {
        fmt.Println("Request user unknown")
    }
    return fn()
}
```

```
Request ID = 1234
Request user = admin
42
Request ID unknown
Request user unknown
42
```

Both `context.WithValue()` and `Context.Value()` work with values of type `any` (they were added to the standard library long before generics):

```go
func WithValue(parent Context, key, val any) Context

type Context interface {
    // ...
    Value(key any) any
}
```

I'm mentioning this feature for completeness, but it's generally better to avoid passing values in context. It's better to use explicit parameters or custom structs instead.

---

## Summary

Use context to safely cancel and timeout operations in a concurrent environment. It's a perfect fit for remote calls, pipelines, or other long-running operations.

Now you know how to:

* Manually cancel operations.
* Cancel on timeout or deadline.
* Use child contexts to restrict timeouts.
* Specify the cancellation reason.
* Register functions to execute when the context is canceled.

Context provides a powerful and standardized way to manage cancellation and timeouts across your concurrent Go programs.


