The Secret Life of Go: The Context Package
Mastering context.Context, timeouts, and cancellation propagation.
Chapter 22: Signals and Timeouts
Ethan was watching the scrolling logs on his terminal. "That’s strange," he said.
"What do you see?" Eleanor asked, pausing by his desk.
"I have a report generation endpoint that takes about ten seconds to run," Ethan explained. "I clicked the button to test it, realized I made a mistake, and closed the browser tab immediately. But look at the logs."
He pointed to the screen. The logs showed the server was still crunching numbers, querying the database, and formatting the PDF.
"The user is gone," Ethan said. "But the server is still working."
"That is because the server does not know the user is gone," Eleanor said gently. "You have started a process, but you have no way to stop it."
"I thought closing the connection would stop it," Ethan said.
"Closing the TCP connection stops the response from being delivered," Eleanor corrected him. "But your Go functions—the database queries, the calculations—they are still running on the CPU. They need to be explicitly told to stop."
The Context Object
"In Go, we solve this with the context package," Eleanor continued. "A context.Context is an object that carries three important things across API boundaries: deadlines, cancellation signals, and request-scoped values."
"So it connects the request to the work?"
"Exactly. By convention, it is always the first argument in a function. This makes it immediately visible that this function can be cancelled or timed out."
She pulled up Ethan's slow function.
The Problem Code:
func GenerateReport() {
// This runs for 10 seconds, regardless of the user
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
fmt.Println("Processing part", i)
}
fmt.Println("Report complete")
}
"We need to change this function to accept a context.Context," Eleanor said. "And inside the loop, we need to check if that context has been cancelled."
The Solution:
func GenerateReport(ctx context.Context) {
for i := 0; i < 10; i++ {
// Check for cancellation before doing work
select {
case <-ctx.Done():
// ctx.Err() tells us WHY (Canceled or DeadlineExceeded)
fmt.Println("Halted:", ctx.Err())
return // Stop immediately
default:
// The 'default' case allows the loop to continue
// without waiting for the channel to close.
}
time.Sleep(1 * time.Second)
fmt.Println("Processing part", i)
}
fmt.Println("Report complete")
}
"The select statement is key here," Eleanor explained. "It looks at the ctx.Done() channel. If that channel is closed, it means the context is cancelled. We return immediately, saving resources."
"And what happens in the default case?" Ethan asked.
"It prevents the select from blocking," Eleanor replied. "If the context is not done, it falls through to default, and your loop continues to the next step."
Wiring It Up
"Now," Eleanor said, "we need to pass the context from the HTTP request down to your function."
Ethan updated his handler.
func HandleReport(w http.ResponseWriter, r *http.Request) {
// The http.Request already has a context attached to it.
// If the user closes the tab, this context is automatically cancelled.
ctx := r.Context()
GenerateReport(ctx)
}
Ethan ran the server again. He started the request, waited two seconds, and closed the tab.
Terminal:Processing part 0Processing part 1Halted: context canceled
"It stopped!" Ethan said. "As soon as I closed the tab, the loop exited."
Setting a Deadline (Timeout)
"There is one more safety measure we should add," Eleanor noted. "Even if the user doesn't close the tab, do we want this report to run forever if something gets stuck?"
"No," Ethan said. "It should probably time out after five seconds."
"We can enforce that by wrapping the context," Eleanor said. "We use context.WithTimeout."
func HandleReport(w http.ResponseWriter, r *http.Request) {
// Create a new context that is a child of the request context
// It will cancel if the user leaves OR if 5 seconds pass
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
// Always call cancel to release the timer resources!
defer cancel()
GenerateReport(ctx)
}
"Why do I need defer cancel() if it times out automatically?" Ethan asked.
"Because WithTimeout creates internal resources to track the clock," Eleanor explained. "If the function finishes early (before the timeout), those resources hang around until the timeout expires. Calling cancel() cleans them up immediately."
Ethan adjusted the timeout to 2 seconds to test it. He kept the tab open.
Terminal:Processing part 0Processing part 1Halted: context deadline exceeded
"It stopped automatically," Ethan observed.
"Exactly," Eleanor smiled. "Now your server is efficient. It only works when there is a user waiting, and it never works longer than you allow."
Key Concepts
context.Context
An interface that carries deadlines, cancellation signals, and request-scoped values across API boundaries.
Cancellation Propagation
- Mechanism: The
ctx.Done()method returns a channel. When a context is cancelled, this channel closes. - Checking: Use
select { case <-ctx.Done(): ... default: ... }to check for cancellation without blocking. - Errors:
ctx.Err()returnscontext.Canceled(manual cancel) orcontext.DeadlineExceeded(timeout).
Creating Contexts
context.Background(): The empty, starting context (usually used inmain).r.Context(): In web servers, the context attached to the incoming HTTP request.
Timeouts and Deadlines
context.WithTimeout(parent, duration): Returns a copy of the parent context that automatically cancels after the duration.- Cleanup: Always call the
cancelfunction returned byWithTimeout(usingdefer) to release timer resources immediately.
Best Practices
- Pass
context.Contextas the first argument to functions. - Never store a Context inside a struct type; pass it explicitly to each function.
Next Episode: Concurrency Patterns. Ethan learns how to manage multiple goroutines safely using sync.WaitGroup and errgroup.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
.jpeg)

Comments
Post a Comment