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
125: time.Sleep and Context-Based Timeouts
I was digging through some old code the other day and found a common pattern that drives me crazy: using time.Sleep to "handle" potential delays in external API calls. It's a blunt instrument. If you tell a goroutine to sleep for five seconds, it's gone. It isn't listening to the world; it's just paused. In a production environment, that's a recipe for leaked goroutines and unresponsive services.
The blunt instrument approach
Let's look at a typical scenario. Suppose we're writing a function to fetch a user's profile from a slow legacy database. To simulate the lag, I'll use time.Sleep. Here is how I'd initially write this if I weren't thinking about timeouts:
package main
import (
"fmt"
"time"
)
func fetchUserProfile(userID int) string {
// Simulate a very slow database query
time.Sleep(5 * time.Second)
return fmt.Sprintf("Profile data for user %d", userID)
}
func main() {
fmt.Println("Fetching profile...")
profile := fetchUserProfile(123)
fmt.Println(profile)
}
If I run this, it works—eventually. But here's the problem: the main function is completely blocked. If this were a web server and the database started lagging, every single incoming request would just sit there for five seconds. If the user refreshes their browser or cancels the request, our server is still stuck in that time.Sleep, wasting resources.
Trying to "force" a timeout
You might think, "Why not just run the fetch in a goroutine and use a timer in the main thread?" I tried that once. You end up with a "race" where you might ignore the result of the function, but the fetchUserProfile goroutine is still sleeping in the background. It doesn't know it's been abandoned. In Go, we don't kill goroutines from the outside; we tell them to stop themselves. That's where context comes in.
Making the function responsive
To fix this, I need to pass a context.Context into the function. Instead of a blind sleep, I'll use a select statement. This allows the function to wait for the "work" to finish OR for the context to tell it to give up.
Let's rewrite the fetch function. I'll replace time.Sleep with a channel that signals when the "work" is done, and I'll listen to ctx.Done().
package main
import (
"context"
"fmt"
"time"
)
func fetchUserProfile(ctx context.Context, userID int) (string, error) {
// We create a channel to simulate the completion of the work
done := make(chan string)
go func() {
// Simulate the slow work here
time.Sleep(5 * time.Second)
done <- fmt.Sprintf("Profile data for user %d", userID)
}()
// This is the magic part
select {
case res := <-done:
return res, nil
case <-ctx.Done():
// The context timed out or was cancelled
return "", ctx.Err()
}
}
func main() {
// Create a context that automatically cancels after 2 seconds
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // Always call cancel to release resources!
fmt.Println("Fetching profile with a 2s timeout...")
profile, err := fetchUserProfile(ctx, 123)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println(profile)
}
What actually happened here?
When I run this version, it doesn't wait five seconds. It hits the 2-second limit and immediately prints Error: context deadline exceeded.
I've shifted the control from the function itself to the caller. The main function now decides how long it's willing to wait. Inside fetchUserProfile, the select block is essentially a race. Either the done channel gets a value (success), or the ctx.Done() channel closes (timeout/cancellation). Whichever happens first wins.
One detail you can't miss: defer cancel(). Even if the function finishes successfully in 1 second, the context's timer is still running in the background until it hits 2 seconds. Calling cancel() stops that timer immediately. If you forget this in a high-traffic loop, you'll create a memory leak that's a nightmare to debug.
📋 Practical Task
Exercise: Implementing a Timeout for a Legacy Database Query
You are integrating with a legacy reporting system that occasionally "hangs" for 30 seconds or more. Your application cannot afford to wait that long; it must return an error to the user if the report isn't ready within 3 seconds.
Your Task:
- Create a function
generateReport(ctx context.Context, reportID string) (string, error). - Inside the function, simulate a variable delay using a goroutine and
time.Sleep. To make your testing interesting, use a random sleep duration between 1 and 5 seconds (you can usemath/randor just hardcode a value to test both success and failure). - Use a
selectblock to handle the competition between the report completion and thectx.Done()channel. - In
main, initialize a context with a 3-second timeout usingcontext.WithTimeout. - Ensure you call the
cancelfunction usingdefer. - Print the resulting report string on success, or the context error on timeout.
There are no comments for now.