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
33: The Context Package for Cancellation
I've seen this exact scenario play out in production more times than I care to admit. A developer writes a feature that kicks off a few background tasks to fetch data from different APIs. It works great in testing. But in production, the memory usage slowly climbs over a week until the service crashes. Why? Because they were leaking goroutines.
Take a look at this snippet. It looks harmless—it's just a function that simulates a slow database query running in the background.
package main
import (
"fmt"
"time"
)
func slowQuery() {
fmt.Println("Query started...")
time.Sleep(10 * time.Second) // Simulate a heavy load
fmt.Println("Query finished!")
}
func main() {
go slowQuery()
fmt.Println("Main function is done, moving on!")
time.Sleep(1 * time.Second)
}
The Ghost in the Machine: Goroutine Leaks
If you run this, you'll see "Query started..." and "Main function is done...", and then the program exits. But wait—in a real web server, main doesn't exit; it keeps running to handle other requests. If a user hits an endpoint that triggers slowQuery and then closes their browser tab, that goroutine is still sitting there, sleeping for 10 seconds, consuming memory and resources. Go has no way to "force kill" a goroutine from the outside. You can't just call goroutine.Stop().
This is where the context package comes in. The context.Context type is essentially a signal wire that you pass through your call stack. When the wire is "cut," every function holding a reference to that context knows it's time to pack up and go home.
Teaching Your Functions to Listen
To fix this, we need to change slowQuery so it doesn't just blindly sleep. It needs to listen for a cancellation signal. We do this by passing a context.Context and using a select block to wait for either the work to finish or the context to be cancelled.
package main
import (
"context"
"fmt"
"time"
)
func slowQuery(ctx context.Context) {
fmt.Println("Query started...")
select {
case <-time.After(10 * time.Second):
fmt.Println("Query finished!")
case <-ctx.Done():
fmt.Println("Query cancelled! Cleaning up resources...")
return
}
}
func main() {
// Create a context that we can manually cancel
ctx, cancel := context.WithCancel(context.Background())
go slowQuery(ctx)
time.Sleep(1 * time.Second)
fmt.Println("User disconnected. Cancelling request...")
cancel() // This sends the signal to ctx.Done()
// Give it a moment to print the cleanup message
time.Sleep(100 * time.Millisecond)
}
Here is what changed: I used context.WithCancel, which returns the context and a cancel function. When I call cancel(), it closes the channel returned by ctx.Done(). Inside slowQuery, the select statement is essentially a race. Whichever happens first—the 10-second timer or the cancellation signal—wins. Because we called cancel() after one second, the ctx.Done() case triggers, and the goroutine exits cleanly.
Passing the Baton Down the Chain
In a real-world app, you won't just have one function. You'll have a handler that calls a service, which calls a repository, which calls a database driver. The rule of thumb in Go is: always pass ctx as the first argument to any function that performs I/O or long-running work.
If the top-level request is cancelled, that signal propagates all the way down the chain. If you're using the database/sql package, you'll notice methods like QueryContext. Use those. They are designed to watch the context you pass in, so if the user cancels the request, the Go driver actually tells the database to kill the query on the server side, too. It's a massive win for performance.
📋 Practical Task
Exercise: Implementing a Cancellable Search Aggregator
You are building a search aggregator that fetches results from three different mock providers (e.g., "Google", "Bing", "DuckDuckGo"). Some providers are slower than others.
Your Task:
- Create a function
fetchResults(ctx context.Context, provider string, delay time.Duration) string. This function should simulate work usingtime.Sleeportime.After. If the context is cancelled before the delay finishes, it should return an empty string and print "Cancelled [provider]". - In your
mainfunction, create a context with a timeout of 2 seconds usingcontext.WithTimeout. - Launch three goroutines calling
fetchResultswith different delays:- Provider A: 1 second (Should succeed)
- Provider B: 2.5 seconds (Should be cancelled)
- Provider C: 5 seconds (Should be cancelled)
- Use a
sync.WaitGroupto ensure your program waits for all goroutines to finish (either by completing or by being cancelled) before exiting.
There are no comments for now.