The Secret Life of Go: The Context Package
Source: Dev.to
How to stop runaway goroutines and prevent memory leaks
How to stop runaway goroutines and prevent memory leaks.
Ethan: “I have a memory leak.”
Eleanor: “Do you?” (not looking up)
Ethan: “Well, not a leak, exactly. But look at my dashboard.” (points to a monitor showing a jagged, climbing line) “This is my API. Every time a user searches for a product, I spawn a goroutine to query the database and the recommendation engine. If the user gets bored and closes their browser, my server keeps working.”
Eleanor: “Because you didn’t tell it to stop.” (walks over)
Ethan: “I thought the HTTP handler handled that?”
Eleanor: “It handles the connection. Your goroutines are independent. You fired them and walked away. They are still running, burning CPU cycles for a user who has already left the building. You’re being rude to your server.”
Ethan: “So how do I tell them the user left?”
Eleanor: “You pass the Context.”
The First Argument
Eleanor pulled up a chair.
“In Go,
context.Contextis the standard way to carry deadlines, cancellation signals, and request‑scoped values across API boundaries. It is almost always the first argument of a function.”
She opened Ethan’s code.
// Bad: No way to stop this function once it starts
func SlowDatabaseQuery(id string) string {
time.Sleep(5 * time.Second) // Simulate work
return "Product Details for " + id
}
func HandleSearch(w http.ResponseWriter, r *http.Request) {
// We ignore the request context!
result := SlowDatabaseQuery("12345")
fmt.Fprintln(w, result)
}
“If I hit this endpoint and cancel the request after one second, your
SlowDatabaseQuerystill sleeps for the full five seconds. Multiply that by a thousand users, and your server crashes.”
She refactored the code.
// Good: We accept a Context
func SlowDatabaseQuery(ctx context.Context, id string) (string, error) {
// Use a select statement to listen for cancellation
select {
// …
}
}
Look at the
selectblock. We are racing two things: the work finishing or the context finishing.ctx.Done()is a channel that closes when the context is canceled. If the user disconnects,ctx.Done()fires immediately, and your function returns. You save four seconds of work.
The Timeout (Setting Boundaries)
“That handles cancellation,” Ethan said. “But what if the database is just broken and hangs forever? The user waits, the connection stays open…”
“Then you set a deadline,” Eleanor replied. “Never let a process run forever. We use
context.WithTimeout.”
She created a new example.
func CallExternalAPI() error {
// 1. Create a derived context that dies after 100 ms
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
// 2. ALWAYS defer the cancel function to release resources
defer cancel()
// 3. Pass this strict context to the worker
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://slow-api.com", nil)
client := &http.Client{}
_, err := client.Do(req)
return err
}
“This is aggressive.”
“If the external API is slower than that, we don’t want the answer,” Eleanor said firmly. “This protects your system. If their server hangs, yours doesn’t pile up thousands of waiting connections. You fail fast and move on.”
“And
defer cancel()?”
“Crucial. If the work finishes in 10 ms, the timer is still ticking in the background. Callingcancel()stops the timer and frees the memory immediately. Always defer cancel.”
The Value (Use with Caution)
“I saw
context.WithValuein the docs. Can I use that to pass user objects and config settings down to my functions?”
Eleanor frowned.
“You can, but you usually shouldn’t.
WithValueis for request‑scoped data—like a request ID or an authentication token. It is untyped and invisible. If you use it to pass required function arguments, you make your code opaque.”
“Opaque?”
“If a function needs a database connection, pass it as an argument:
func(db *DB). Do not hide it insidectxwhere no one can see it. Explicit is better than implicit.”
The Chain of Command
Ethan looked at his dashboard. The memory usage was flattening out.
“It propagates,” he realized. “If I cancel the parent context in
HandleSearch, it cancels the child context inSlowDatabaseQuery, which cancels the HTTP request to the database…”
“Exactly,” Eleanor said. “It is a chain of command. When the top level says ‘stop,’ the order goes all the way down the tree. Every function cleans up its own mess and returns.”
She stood up, picked up her stack of punch cards, and said:
“A server is not a trash can, Ethan. Do not fill it with abandoned processes. Be polite. When the user leaves, stop working.”
Key Concepts from Chapter 16
| Concept | What It Does |
|---|---|
context.Context | Carries deadlines, cancellation signals, and request‑scoped values. |
| First argument | Functions that do work should accept a Context as their first parameter. |
select on ctx.Done() | Allows work to stop early when the context is canceled. |
context.WithTimeout / WithDeadline | Enforces a hard upper bound on how long work may run. |
defer cancel() | Releases resources (timers, goroutine leaks) as soon as the work finishes. |
context.WithValue | Use sparingly—for metadata only, not for required arguments. |
| Propagation | Canceling a parent context automatically cancels all derived child contexts. |
By consistently passing and respecting Context, you prevent runaway goroutines, avoid memory leaks, and keep your server responsive.
Go Context Overview
A context carries deadlines, cancellation signals, and request‑scoped values. It is immutable and thread‑safe.
ctx.Done()
- A channel that closes when the context is canceled or times out.
- Use it in a
selectstatement to return early if work is no longer needed.
context.Background()
- The root context.
- Use this when you are starting a main function or a top‑level process and have no existing context to inherit from.
context.WithCancel(parent)
ctx, cancel := context.WithCancel(parent)
- Returns a copy of the parent context with a new
Donechannel. - Calling the returned
cancel()function closes that channel.
context.WithTimeout(parent, duration)
ctx, cancel := context.WithTimeout(parent, duration)
- Returns a copy of the parent that automatically cancels after the specified duration.
Best Practice:
Always defer cancel() to release resources as soon as the function returns, even if the timeout hasn’t fired.
context.TODO()
- A placeholder context.
- Use it when you are refactoring and haven’t decided where the context should come from yet.
context.WithValue
ctx = context.WithValue(parent, key, value)
- Used for passing request‑scoped data (e.g., Trace IDs).
- Do not use it for optional parameters or core dependencies (like loggers or database handles). Explicit arguments are clearer.
Next chapter: JSON and Tags. Ethan tries to parse a configuration file and learns that Go’s struct tags are the closest thing the language has to magic.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.