Skip to Content
Course content

141: context.WithCancel and context.WithTimeout

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you've hired a contractor to renovate your kitchen. You tell them, "Go ahead and start tearing out the old cabinets." But while they're working, you suddenly realize you can't afford the project anymore. You don't just walk away and hope they stop; you call them and say, "Stop everything immediately. Don't spend another dime or another hour on this."

Or, imagine you're ordering a pizza. You're starving, but you have a flight to catch in 40 minutes. You tell the shop, "I want a large pepperoni, but if it's not at my door in 20 minutes, I'm cancelling the order because I'll be at the airport."

In Go, context.WithCancel and context.WithTimeout are exactly those phone calls. They are the mechanism we use to tell a goroutine—which might be deep inside a database driver or a network call—that its work is no longer needed. Without this, you end up with "goroutine leaks," where processes keep running in the background, eating CPU and memory, long after the user has closed their browser tab.

Cutting the Cord Manually

context.WithCancel gives you a way to trigger a shutdown whenever you feel like it. It returns a copy of the parent context and a cancel function. When you call that function, the ctx.Done() channel closes. Any goroutine listening to that channel knows it's time to pack up and go home.

I've seen a lot of developers forget to call the cancel function, which is a mistake. Even if the work finishes successfully, you should call cancel() to release the resources associated with that context. The idiomatic way to do this is with defer.

package main

import (
	"context"
	"fmt"
	"time"
)

func heavyWork(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("Worker: Stopping work because I was cancelled!")
			return
		default:
			fmt.Println("Worker: Still grinding away...")
			time.Sleep(500 * time.Millisecond)
		}
	}
}

func main() {
	// Create a cancelable context
	ctx, cancel := context.WithCancel(context.Background())
	
	// I'm deferring this so we don't leak resources 
	// even if the function panics.
	defer cancel()

	go heavyWork(ctx)

	// Let the worker run for a bit
	time.Sleep(2 * time.Second)
	fmt.Println("Main: I've changed my mind. Stop!")
	cancel() // This triggers <-ctx.Done() in the goroutine

	// Give the worker a moment to print its exit message
	time.Sleep(100 * time.Millisecond)
}

Setting a Hard Deadline

In a production environment, you can't wait forever for an API response. If a third-party service is hanging, you don't want your own users staring at a loading spinner for thirty seconds. This is where context.WithTimeout comes in.

It works exactly like WithCancel, but it adds a timer. The context will be cancelled automatically after the duration expires, OR when you call the cancel function manually—whichever happens first.

package main

import (
	"context"
	"fmt"
	"time"
)

func simulateApiCall(ctx context.Context) {
	select {
	case <-time.After(5 * time.Second): // Simulate a slow network
		fmt.Println("API: Finally finished the request!")
	case <-ctx.Done():
		fmt.Println("API: Request aborted because it took too long.")
	}
}

func main() {
	// We only want to wait 2 seconds for this API
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	go simulateApiCall(ctx)

	// Wait to see what happens
	<-ctx.Done()
	fmt.Println("Main: Moving on...")
}

The "Done" Pattern

The most important thing to remember here is that context is not a magic "kill switch." It cannot forcibly terminate a goroutine. If you write a goroutine that just does for { ... } without ever checking <-ctx.Done(), that goroutine will run forever, regardless of whether you called cancel() or the timeout expired.

You are essentially asking the goroutine to please stop. It's the goroutine's responsibility to be "context-aware" and check for that signal frequently, especially before starting a heavy operation or during a long loop.




📋 Practical Task

Build a Fault-Tolerant Image Processor

You are building a system that processes high-resolution images. Some images are small and fast, while others are massive and might hang your processor. Your goal is to ensure that no single image processing task takes longer than 3 seconds.

Requirements:

  • Create a function processImage(ctx context.Context, imageName string, duration time.Duration).
  • Inside processImage, use a select block to either simulate the work (using time.After with the provided duration) or stop if the context is cancelled.
  • In your main function, trigger two separate image processes:
    • "image_small.jpg" which takes 1 second to process.
    • "image_huge.jpg" which takes 10 seconds to process.
  • Use context.WithTimeout to enforce the 3-second limit for both.
  • Print a message indicating whether each image was "Successfully Processed" or "Timed Out".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.