Golang Concurrency #9 - Context and Cancellations

Just a guy who loves to write code and watch anime.
Search for a command to run...

Just a guy who loves to write code and watch anime.
No comments yet. Be the first to comment.
Qualities that make outstanding builders.

Bipedal vs quadrupedal: how many legs This is about the number of legs the creature walks on. Bipedal. Two legs. Humans, ostriches, T-rex, kangaroos, most fantasy humanoids. Quadrupedal. Four legs. Do

Intro You hear "lerp" and "smoothstep" everywhere in game dev. They sound like math jargon. They're not. Both are small tools that do the same job: smoothly move from one value to another. The problem

What is Kinematics Kinematics is the math field. It's the study of how things move without worrying about forces (which would be dynamics). FK and IK are the two branches: forward kinematics and inver

Intro Textures are usually the biggest cost in a 3D scene. Memory, bandwidth, and load time all get eaten by them. Resizing them is the obvious lever. There's more. This post is about the less obvious

Context solves the problem: "How do I gracefully stop operations or set timeouts across multiple goroutines?"
Context provides cooperative cancellation -> you control when and how to check for cancellation, not abrupt force-killing.
// Cancel automatically after 3 seconds
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // Always call cancel to free resources
// Cancel manually when you want
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Later...
cancel() // Trigger cancellation
ctx := context.Background() // Never cancelled, starting point
func worker(ctx context.Context, name string) {
for { // Infinite loop -> YOU control when to exit
select {
case <-ctx.Done(): // "Did someone cancel me?"
fmt.Printf("%s: Got cancellation signal\n", name)
cleanup() // YOU decide what cleanup to do
fmt.Printf("%s: Cleaned up, exiting gracefully\n", name)
return // YOU decide when to exit
default: // "No cancellation signal, keep working"
fmt.Printf("%s: working...\n", name)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
// Pass same context to multiple workers
go worker(ctx, "Worker 1")
go worker(ctx, "Worker 2")
go worker(ctx, "Worker 3")
time.Sleep(2 * time.Second)
cancel() // Signal ALL workers to cancel
time.Sleep(1 * time.Second) // Give them time to clean up
}
Workers start: Enter infinite for loop
Select checks: "Is ctx.Done() ready to receive?"
If NOT cancelled: Goes to default, does work, loops again
When cancel() called: ctx.Done() channel gets closed
Next time workers hit select: ctx.Done() case executes
Workers clean up: Do whatever cleanup they want
Workers exit: When THEY decide to return
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return // Check every loop iteration
default:
}
doWork()
}
}
func worker(ctx context.Context) {
for i := 0; i < 1000000; i++ {
processItem(i)
// Only check cancellation every 1000 items
if i%1000 == 0 {
select {
case <-ctx.Done():
fmt.Printf("Cancelled at item %d\n", i)
return
default:
}
}
}
}
func fetchData(ctx context.Context) (string, error) {
result := make(chan string)
go func() {
time.Sleep(5 * time.Second) // Simulate slow work
result <- "data"
}()
select {
case data := <-result:
return data, nil // Work finished first
case <-ctx.Done():
return "", ctx.Err() // Cancellation won first
}
}
func main() {
// Context automatically cancels after 2 seconds
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
result := make(chan string)
go slowWork(ctx, result)
select {
case data := <-result:
fmt.Println("Got result:", data)
case <-ctx.Done():
fmt.Println("Timeout! Work took too long")
fmt.Println("Error:", ctx.Err()) // context.DeadlineExceeded
}
}
func slowWork(ctx context.Context, result chan string) {
select {
case <-time.After(3 * time.Second): // Work takes 3 seconds
result <- "finished work"
case <-ctx.Done(): // But context times out after 2 seconds
fmt.Println("Work cancelled due to timeout")
return
}
}
func handleRequest(ctx context.Context) {
// Pass context down the call chain
data := fetchFromAPI(ctx)
processData(ctx, data)
saveToDatabase(ctx, data)
}
func fetchFromAPI(ctx context.Context) string {
// This function also respects the same cancellation
select {
case <-time.After(1 * time.Second):
return "api data"
case <-ctx.Done():
return ""
}
}
Context doesn't force-kill anything. Your goroutines choose when to check and how to respond.
You can clean up, save state, close files, etc. before exiting.
One context can control an entire tree of operations.
<-ctx.Done() receives from the Done() channel, which becomes ready when context is cancelled.
cancel()Even with timeout contexts, call cancel() to free resources.