JavaRush/Courses/Go SELF/context.Context: Background, TODO, WithCancel

context.Context: Background, TODO, WithCancel

26 level, 4 lecture
Open

1. Why context is needed: “I changed my mind” as part of the program

When you write learning programs, it can seem like code always runs “whoosh—and it’s done.” But in real tasks, many operations can take noticeable time: reading a large stream, importing data, making a network request, working with the file system, a long computation. And at some point, the human “I changed my mind” appears: the user pressed Ctrl+C, the server closed the connection, a timeout happened, or the UI switched to another screen.

You can, of course, say: “Let it keep loading.” But the program has memory, descriptors, buffers, and sometimes money (if you pay for external APIs). That’s why Go provides a standard “common language” to tell a function: stop. This language is where context.Context lives: it carries a “cancellation signal” and the reason for stopping across function and package boundaries. And it’s not a “nice-to-have for prettiness,” but a foundation of resilient servers and libraries.

The context.Context interface: what matters in it

context.Context is an interface (a contract) that carries information about an operation. At an introductory level, it’s enough to think of it as a small object with two useful ideas:

  1. an operation can have a “canceled” state,
  2. if it’s canceled, we can learn the reason via Err().

The official description of context includes the Done() channel, deadlines, and values, but today we take the most practical minimum: Background, TODO, WithCancel, cancel(), and checking Err(). At the same time, it’s important to understand the core principle: a context is not a “kill the thread” command, but a signal that your function must respect. That is, cancellation works only where you regularly check the context and voluntarily stop the work.

A small “map of the terrain” table to avoid getting confused:

Thing What is it? How it sounds in plain English
context.Background()
root context “I’m starting an operation from scratch”
context.TODO()
placeholder “A context is needed, but I haven’t decided which one yet”
context.WithCancel(parent)
creates a “child” context + cancel() “Here’s an operation that can be canceled manually”
ctx.Err()
cancellation reason or nil “Are we canceled? If yes—why?”

2. Basic contexts: Background and TODO

context.Background(): the root of the context tree

Let’s start with the simplest and calmest one: context.Background().

It’s convenient to perceive it as a root point: a “tree” of contexts is built from it. Such a context never cancels itself and is usually used in main() or as a “default context” if there’s nowhere else to take one from yet.

A mini-example: we simply ask for Err()—it will return nil, because nothing is canceled.

package main

import (
	"context"
	"fmt"
)

func main() {
	ctx := context.Background()
	fmt.Println(ctx.Err()) // <nil>
}

An important meaning: Background() is not a magic button. It’s just a standard, “forever” parent. If you create Background() inside every function, cancellation will never propagate “downward,” and the whole point of context will be lost.

context.TODO(): a placeholder during development

context.TODO() is often perceived as “another Background().” Technically, in behavior it is similar: it’s also not canceled, Err() is also nil. But in meaning it’s different: it’s a marker for the programmer and reviewer—“a context is needed here, but I haven’t figured out where to get it yet.”

This is useful in intermediate code when you’re still designing the API. For example, you wrote a function that already requires a context, but the calling site isn’t ready yet—you put TODO() and later come back to “weave in” the real context from a request/CLI command.

package main

import (
	"context"
	"fmt"
)

func main() {
	ctx := context.TODO()
	fmt.Println(ctx.Err()) // <nil>
}

A joke (with a grain of truth): TODO() is a “sticky note in the code.” It doesn’t solve the problem, but it makes the problem visible.

4. Cancellation via WithCancel and Err()

context.WithCancel: making a cancelable context

Now for the most interesting part: context.WithCancel(parent).

This function takes a parent context and returns two values: a new ctx and a cancel function. When you call cancel(), the context transitions to the “canceled” state, and ctx.Err() starts returning the cancellation error.

A mini-example: cancel immediately and print Err().

package main

import (
	"context"
	"fmt"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	fmt.Println(ctx.Err()) // context canceled
}

Why call cancel() even without explicit cancellation

This is a subtle point that beginners often miss: cancel() is worth calling (usually via defer cancel()), because the context can hold internal resources (service structures, parent→child links). In good Go style, whoever created WithCancel also “closes” its lifecycle by calling cancel(). It’s like defer file.Close(): even if “the file is small,” you should still close it.

Here’s a typical template:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// ... work with ctx ...

How to respect cancellation: check ctx.Err() in long-running places

If a context can be canceled, a logical question arises: “How does my function know it was canceled?”

At an introductory level, the simplest and clearest way is to periodically check ctx.Err() in a loop or between algorithm steps. If Err() returned non-nil, the work should end—usually by returning that same error.

This is especially handy in what we already know: in I/O loops (reading in chunks) or in record-processing loops.

A small “consume stream in chunks” function that respects cancellation:

package main

import (
	"context"
	"fmt"
	"io"
)

func consume(ctx context.Context, r io.Reader) error {
	buf := make([]byte, 4)

	for {
		if err := ctx.Err(); err != nil {
			return err
		}

		n, err := r.Read(buf)
		if n > 0 {
			fmt.Printf("got %q\n", buf[:n]) // e.g.: got "abcd"
		}
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
	}
}

Notice an important habit: we first use buf[:n], and only then decide what to do with err. This works well with cancellation: the context is checked “at the top” of each iteration, while the read remains correct.

5. Embedding context into an API and an application

ctx is the first parameter

In Go there is an established convention: if a function supports cancellation, it accepts ctx context.Context as the first parameter. This is not “style for style’s sake,” but a way to make code readable: you immediately see that the operation is context-aware and can end early.

Bad signature (it’s confusing and hides what matters):

func ImportTasks(r io.Reader, ctx context.Context) error { /* ... */ }

Good signature (as in the standard library and most projects):

func ImportTasks(ctx context.Context, r io.Reader) error { /* ... */ }

Another rule closely related to this: don’t pass nil instead of a context. If “there is no context,” use context.Background() (or TODO() as a temporary marker). This way you avoid panic and remove unnecessary branching: the function shouldn’t have to guess what you meant by passing nil.

Mini-example: importing tasks with cancellation

So the topic doesn’t stay abstract, let’s carefully continue our learning app about tasks. Earlier we already had a Task type, and we learned how to print results into an io.Writer. Now imagine we added task import from any io.Reader: for example, from memory (bytes.Buffer) or from another source.

We don’t use bufio.Scanner (that’s a separate topic), so we’ll take a simple approach: read the stream in chunks, append into a bytes.Buffer, and then parse lines.

First, the model:

package main

type Task struct {
	Title string
}

Now the import function that respects cancellation via ctx.Err():

package main

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"strings"
)

func ImportTasks(ctx context.Context, r io.Reader) ([]Task, error) {
	var all bytes.Buffer
	buf := make([]byte, 8)

	for {
		if err := ctx.Err(); err != nil {
			return nil, err
		}

		n, err := r.Read(buf)
		if n > 0 {
			all.Write(buf[:n])
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("read tasks: %w", err)
		}
	}

	lines := strings.Split(strings.TrimSpace(all.String()), "\n")
	tasks := make([]Task, 0, len(lines))
	for _, line := range lines {
		if line == "" {
			continue
		}
		tasks = append(tasks, Task{Title: line})
	}
	return tasks, nil
}

There are several important ideas here that “glue” the whole module together:

  • We read as a stream and use only buf[:n], as we agreed earlier.
  • We check ctx.Err() on every read iteration. This makes the import “cancelable,” even if the io.Reader itself knows nothing about contexts.
  • We wrap the read error with %w, so higher up the stack it’s clear that it happened specifically at the read tasks step. This continues good error style: add operation context, but don’t lose the root cause.

A mini-demo where we cancel the operation before it starts:

package main

import (
	"bytes"
	"context"
	"fmt"
)

func main() {
	var src bytes.Buffer
	src.WriteString("buy milk\nlearn go\n")

	ctx, cancel := context.WithCancel(context.Background())
	cancel() // canceled in advance

	tasks, err := ImportTasks(ctx, &src)
	fmt.Println("tasks:", tasks) // tasks: []
	fmt.Println("err:", err)     // err: context canceled
}

Of course, in a real program cancellation often happens “from the outside” (user, server, timeout), but it’s important for us to see the mechanics: if the context is canceled, the function stops quickly and predictably.

6. Context tree: cancellation propagates downward

Now let’s reinforce one idea that often becomes an “aha moment”: contexts form a tree. If you created a context from a parent and the parent is canceled, then all “children” are canceled too. This is the fundamental model of context.

As a simple diagram:

flowchart TD
    A[context.Background] --> B[WithCancel: ctx1]
    B --> C[WithCancel: ctx2]
    B --> D[WithCancel: ctx3]

    X[cancel ctx1] --> B
    B -->|cancellation propagates| C
    B -->|cancellation propagates| D

This model is especially useful when one “upper” level starts an operation and lower levels do sub-operations. Even without diving deep into concurrency, you can understand the main point: one cancellation signal should neatly “flow” through all functions participating in the operation.

7. Common mistakes when working with context

Mistake #1: creating context.Background() inside every function.
It looks harmless: “I need a context, so I created one.” But this breaks the cancellation chain. The calling code cancels its ctx, and your function keeps working because it has a different, “forever” Background. The correct approach is to accept ctx as a parameter and pass it along.

Mistake #2: forgetting to call cancel() after WithCancel.
Even if it seems like “it will end anyway,” good style is to defer cancel() where the context is created. This disciplines the code and helps avoid holding unnecessary internal links and resources longer than needed. In feeling, it’s like “forgetting to close a file”: sometimes nothing breaks, and sometimes you get very strange effects.

Mistake #3: passing nil instead of a context.
Beginners sometimes think: “If a context isn’t needed, I’ll pass nil.” And then someone calls ctx.Err() and gets a panic. In Go, the convention is: no context—use context.Background() (or TODO() as a temporary placeholder). This makes the code linear and safe.

Mistake #4: not checking cancellation in long loops.
You added ctx to the signature, proudly passed it through all functions, but forgot to do ctx.Err() anywhere. As a result, cancellation doesn’t work: formally there is support, but in practice there isn’t. The simplest way to start respecting the context is to put a check in “natural points”: on loop iterations, between processing steps, before expensive operations.

Mistake #5: confusing “context” with “application data.”
There is a temptation to treat context as a “bag you can shove everything into”: current user, currency, sorting settings... But at an introductory level it’s better to keep the focus: today we need context as a cancellation mechanism for an operation. The less you try to turn it into a “global variable on steroids,” the easier the code is to maintain.

1
Task
Go SELF,  26 level,  4 lecture
Locked
Empty contexts
Empty contexts
1
Task
Go SELF,  26 level,  4 lecture
Locked
Manual cancellation
Manual cancellation
1
Task
Go SELF,  26 level,  4 lecture
Locked
Reading with Cancellation
Reading with Cancellation
1
Task
Go SELF,  26 level,  4 lecture
Locked
Parent and child
Parent and child
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet