Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
140: context.Background and context.TODO
By now, you've probably noticed that almost every meaningful function in a professional Go codebase takes ctx context.Context as its first argument. It's become the standard way we handle timeouts, cancellations, and request-scoped values. But this leads to a classic "chicken and egg" problem: if every function needs a context passed into it, who actually creates the first one?
The trap of the internal Background
When I'm reviewing code from developers new to Go, I often see a pattern where they realize a library function requires a context, but they don't want to "clutter" their own function signature by adding it. To solve this, they do something like this:
func GetUserPermissions(userID string) ([]string, error) {
// "I just need a context to make this call work"
ctx := context.Background()
return db.QueryPermissions(ctx, userID)
}
On the surface, this works. The code compiles, the query runs, and the tests pass. But you've just created a "detached" context. By calling context.Background() inside the function, you've severed the link between the caller and the operation. If the user closes their browser tab or the HTTP request times out, the GetUserPermissions function has no way of knowing. It will keep churning away at the database, wasting resources on a result that nobody is listening for anymore.
The better way is to embrace the "clutter." If QueryPermissions needs a context, GetUserPermissions should probably take one too. You pass the context down the chain from the very top—usually from the incoming HTTP request or the main entry point of your CLI tool. This ensures that a cancellation signal at the top ripples all the way down to the database driver.
Being honest with context.TODO
Now, you might be wondering why context.TODO() even exists if context.Background() does the same thing technically. If you look at the source code, they are virtually identical. The difference isn't technical; it's communicative.
I use context.Background() when I am certain this is the root of the tree. This is for your main() function, your top-level background workers, or the start of a test case. It says, "This is the intended starting point."
I use context.TODO() as a marker. Imagine you're refactoring a legacy project. You're adding context support to a deep call stack of twenty functions. You can't update all twenty signatures in one commit without breaking everything, but you need to update the leaf function (the one actually doing the I/O) right now.
func LegacyWrapper() {
// I know this should be passed in, but I haven't
// updated the callers yet.
result, err := FetchData(context.TODO())
// ...
}
By using TODO(), you're leaving a breadcrumb for yourself and your teammates. When someone searches the codebase for ".TODO()", they'll find every spot where the context chain is broken. If you used Background() there, it would look intentional, and you'd likely never go back to fix the leak. It's essentially a compiler-approved "FIXME" comment.
📋 Practical Task
Refactoring the Leaky Permission Checker
You have a small piece of a system where a PermissionService is leaking database connections because it creates its own background contexts instead of respecting the request lifecycle. Your task is to refactor the code to properly propagate the context from the handler down to the data layer.
Requirements:
- Modify
GetPermissionsto accept acontext.Contextargument. - Remove the local call to
context.Background()insideGetPermissionsand use the passed-in context instead. - Update the
Handlerfunction to pass the request context (viar.Context()) into the service. - In the
mainfunction, usecontext.Background()to initialize the starting context for the simulation.
package main
import (
"context"
"fmt"
"net/http"
)
type PermissionService struct{}
// TODO: Refactor this to stop using context.Background() internally
func (s *PermissionService) GetPermissions(userID string) ([]string, error) {
ctx := context.Background()
fmt.Printf("Fetching permissions for %s using context %v\n", userID, ctx)
// Simulate a DB call that respects ctx
return []string{"read", "write"}, nil
}
func Handler(w http.ResponseWriter, r *http.Request) {
svc := &PermissionService{}
// TODO: Pass the request context here
perms, err := svc.GetPermissions("user-123")
if err != nil {
http.Error(w, "error", 500)
return
}
fmt.Fprintf(w, "Permissions: %v", perms)
}
func main() {
// The entry point: This is where context.Background() actually belongs
fmt.Println("Server starting...")
// Simulation of a request call
// ...
}
There are no comments for now.