time and context Recipes

Updated

September 8, 2026

time and context Recipes

Overview

time measures and schedules; context propagates deadlines and cancel across API boundaries. Together they implement request budgets.

Language-oriented time intro: Time. Concurrency-focused context: Context.

time essentials

now := time.Now()
t := time.Date(2026, 8, 3, 12, 0, 0, 0, time.UTC)
d := 1500 * time.Millisecond
d, err := time.ParseDuration("1h30m")

Format and parse

Go’s reference time is Mon Jan 2 15:04:05 MST 2006 (layout by example):

const layout = time.RFC3339
s := now.UTC().Format(layout)
t, err := time.Parse(layout, s)

Prefer RFC3339 / RFC3339Nano for APIs.

Timers and tickers

timer := time.NewTimer(2 * time.Second)
defer timer.Stop()
select {
case <-timer.C:
case <-ctx.Done():
    if !timer.Stop() {
        <-timer.C // drain if already fired (pre-1.23 habit; see release notes)
    }
}

ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
    select {
    case <-ctx.Done():
        return ctx.Err()
    case t := <-ticker.C:
        _ = t
    }
}

Always Stop tickers to avoid leaks.

context essentials

ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

ctx, cancel = context.WithTimeout(ctx, 3*time.Second)
defer cancel()

ctx, cancel = context.WithDeadline(ctx, time.Now().Add(time.Second))
defer cancel()

Values (use sparingly)

type key int
const requestID key = 1
ctx = context.WithValue(ctx, requestID, "abc")
id, _ := ctx.Value(requestID).(string)

Store request-scoped metadata (IDs), not optional parameters that belong in function args.

Waiting on cancel

select {
case <-ctx.Done():
    return ctx.Err() // context.Canceled or DeadlineExceeded
case res := <-results:
    return res, nil
}

Recipes

Bound a function

func fetch(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }
    resp, err := http.DefaultClient.Do(req)
    // ...
}

Remaining budget

if dl, ok := ctx.Deadline(); ok {
    remaining := time.Until(dl)
    if remaining < 50*time.Millisecond {
        return errBudgetExhausted
    }
}

Sleep that respects cancel

func sleep(ctx context.Context, d time.Duration) error {
    t := time.NewTimer(d)
    defer t.Stop()
    select {
    case <-ctx.Done():
        return ctx.Err()
    case <-t.C:
        return nil
    }
}

Runnable example

go mod init example
go run .
package main

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

func work(ctx context.Context, name string, d time.Duration) error {
    select {
    case <-time.After(d):
        fmt.Println(name, "done")
        return nil
    case <-ctx.Done():
        fmt.Println(name, "canceled:", ctx.Err())
        return ctx.Err()
    }
}

func main() {
    now := time.Now().UTC()
    fmt.Println("rfc3339:", now.Format(time.RFC3339))
    d, _ := time.ParseDuration("250ms")
    fmt.Println("duration:", d)

    ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
    defer cancel()

    _ = work(ctx, "slow", 500*time.Millisecond)

    ctx2, cancel2 := context.WithTimeout(context.Background(), time.Second)
    defer cancel2()
    _ = work(ctx2, "fast", 50*time.Millisecond)

    if dl, ok := ctx2.Deadline(); ok {
        fmt.Println("remaining>0?", time.Until(dl) > 0)
    }
}

Expected output:

rfc3339: 2026-...
duration: 250ms
slow canceled: context deadline exceeded
fast done
remaining>0? true

What to notice: - Timeout cancels waiters blocked in select on ctx.Done(). - Always defer cancel() to free timer resources even on success. - Format APIs with RFC3339 unless you have a domain-specific layout.

Try next: Nest a child WithTimeout shorter than the parent and observe which deadline fires first.