Understanding Timeouts in Go Programming
Timeouts are essential for building robust and responsive applications. This guide covers everything you need to know about implementing and managing timeouts effectively in Go.
Timeout Basics
Basic Timeout Patterns
// Channel timeout
select {
case result := <-ch:
fmt.Println("Received:", result)
case <-time.After(time.Second):
fmt.Println("Timeout")
}
// Context timeout
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
select {
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("Operation timed out")
}
case result := <-doWork(ctx):
fmt.Println("Result:", result)
}
Timeout with Channels
func withTimeout(timeout time.Duration) (string, error) {
ch := make(chan string, 1)
go func() {
result := longRunningOperation()
ch <- result
}()
select {
case result := <-ch:
return result, nil
case <-time.After(timeout):
return "", errors.New("operation timed out")
}
}
// Usage
result, err := withTimeout(2 * time.Second)
if err != nil {
log.Fatal(err)
}
Best Practices
1. Context Timeouts
func processWithContext(ctx context.Context) error {
// Create derived context with timeout
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
done := make(chan error, 1)
go func() {
done <- processData(ctx)
}()
select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err()
}
}
// Usage in HTTP handler
func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if err := processWithContext(ctx); err != nil {
if err == context.DeadlineExceeded {
http.Error(w, "Request timed out", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
2. Cleanup on Timeout
func processWithCleanup(ctx context.Context) error {
cleanup := func() {
// Cleanup resources
}
done := make(chan error, 1)
go func() {
defer cleanup()
done <- processData(ctx)
}()
select {
case err := <-done:
return err
case <-ctx.Done():
cleanup()
return ctx.Err()
}
}
3. Configurable Timeouts
type Config struct {
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
}
type Server struct {
config Config
}
func (s *Server) processRequest(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, s.config.ReadTimeout)
defer cancel()
data, err := s.read(ctx)
if err != nil {
return fmt.Errorf("read error: %w", err)
}
ctx, cancel = context.WithTimeout(ctx, s.config.WriteTimeout)
defer cancel()
return s.write(ctx, data)
}
Common Patterns
1. HTTP Client Timeouts
// Complete HTTP client with timeouts
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
IdleConnTimeout: 90 * time.Second,
},
}
// Usage with context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com", nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
2. Database Timeouts
type DB struct {
conn *sql.DB
timeout time.Duration
}
func (db *DB) QueryWithTimeout(query string, args ...interface{}) (*sql.Rows, error) {
ctx, cancel := context.WithTimeout(context.Background(), db.timeout)
defer cancel()
return db.conn.QueryContext(ctx, query, args...)
}
// Usage with connection pool
func NewDB(dsn string) (*DB, error) {
conn, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
conn.SetConnMaxLifetime(time.Minute * 3)
conn.SetMaxOpenConns(10)
conn.SetMaxIdleConns(10)
return &DB{
conn: conn,
timeout: 5 * time.Second,
}, nil
}
3. Worker Pool with Timeouts
type WorkerPool struct {
workers int
timeout time.Duration
jobs chan Job
results chan Result
done chan struct{}
}
func (p *WorkerPool) worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case job := <-p.jobs:
ctx, cancel := context.WithTimeout(ctx, p.timeout)
result := p.processJob(ctx, job)
cancel()
select {
case p.results <- result:
case <-ctx.Done():
return
}
}
}
}
func (p *WorkerPool) processJob(ctx context.Context, job Job) Result {
done := make(chan Result, 1)
go func() {
done <- job.Process()
}()
select {
case result := <-done:
return result
case <-ctx.Done():
return Result{Error: ctx.Err()}
}
}
Performance Considerations
1. Timer Management
// Bad: Creates new timer for each operation
func badTimerUsage() {
for {
select {
case <-time.After(time.Second):
// Handle timeout
case data := <-dataChan:
// Process data
}
}
}
// Good: Reuse timer
func goodTimerUsage() {
timer := time.NewTimer(time.Second)
defer timer.Stop()
for {
timer.Reset(time.Second)
select {
case <-timer.C:
// Handle timeout
case data := <-dataChan:
// Process data
}
}
}
2. Context Chain Depth
// Bad: Deep context chain
func deepContextChain() error {
ctx := context.Background()
ctx1, cancel1 := context.WithTimeout(ctx, 5*time.Second)
defer cancel1()
ctx2, cancel2 := context.WithTimeout(ctx1, 3*time.Second)
defer cancel2()
ctx3, cancel3 := context.WithTimeout(ctx2, 1*time.Second)
defer cancel3()
return doWork(ctx3)
}
// Good: Use appropriate timeout level
func appropriateTimeout() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
return doWork(ctx)
}
Common Mistakes
1. Not Canceling Context
// Wrong: Context not canceled
func wrong() {
ctx, _ := context.WithTimeout(context.Background(), time.Second)
// Context leak: cancel function not called
doWork(ctx)
}
// Right: Always cancel context
func right() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() // Ensures context is canceled
doWork(ctx)
}
2. Incorrect Timeout Handling
// Wrong: Timeout doesn't cancel operation
func wrongTimeout() {
done := make(chan bool)
go func() {
longRunningOperation()
done <- true
}()
select {
case <-done:
return
case <-time.After(time.Second):
return // Operation continues in background
}
}
// Right: Operation can be canceled
func rightTimeout(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
return longRunningOperation(ctx)
}
3. Resource Leaks
// Wrong: Resources not cleaned up on timeout
func wrongCleanup() {
resource := acquireResource()
select {
case result := <-process(resource):
return result
case <-time.After(time.Second):
return fmt.Errorf("timeout") // Resource leak
}
}
// Right: Ensure cleanup
func rightCleanup() {
resource := acquireResource()
defer releaseResource(resource)
select {
case result := <-process(resource):
return result
case <-time.After(time.Second):
return fmt.Errorf("timeout")
}
}
Next Steps
- Learn about context
- Explore channels
- Study error handling
- Practice with worker pools