4 min read
On this page

Time & Context

The time package handles dates, durations, and timers. The context package provides cancellation, deadlines, and request-scoped values. Together, they are essential for writing Go services that handle timeouts, graceful shutdowns, and coordinated cancellation across goroutines.

time.Time, time.Duration & time.Now

time.Time represents an instant in time. time.Duration represents an elapsed span.

func main() {
    now := time.Now()
    fmt.Println("Current time:", now)

    // Create a specific time
    release := time.Date(2024, time.March, 15, 10, 0, 0, 0, time.UTC)
    fmt.Println("Release:", release)

    // Duration arithmetic
    elapsed := now.Sub(release)
    fmt.Println("Time since release:", elapsed)

    // Add a duration to a time
    deadline := now.Add(24 * time.Hour)
    fmt.Println("Deadline:", deadline)

    // Compare times
    fmt.Println("Past release?", now.After(release))
}
Current time: 2026-04-18 14:30:00.123456 -0700 PDT
Release: 2024-03-15 10:00:00 +0000 UTC
Time since release: 18264h30m0.123456s
Deadline: 2026-04-19 14:30:00.123456 -0700 PDT
Past release? true

Durations are defined as constants.

timeout := 30 * time.Second
interval := 5 * time.Minute
maxAge := 24 * time.Hour
shortDelay := 100 * time.Millisecond

Never multiply a duration by a duration. To compute "5 times a duration," use integer multiplication.

// Correct
d := 5 * time.Second

// Wrong — this multiplies nanoseconds by nanoseconds
d := time.Second * time.Second

Formatting: The Reference Time

Go uses a unique approach to time formatting. Instead of format codes like %Y-%m-%d, Go uses a reference time that you rearrange into the desired layout.

The reference time is: Mon Jan 2 15:04:05 MST 2006

Each component has a specific value: month 1, day 2, hour 15 (3 PM), minute 04, second 05, year 2006, timezone MST. Rearrange these components to define your format.

func main() {
    now := time.Now()

    // Common formats
    fmt.Println(now.Format("2006-01-02"))
    fmt.Println(now.Format("2006-01-02 15:04:05"))
    fmt.Println(now.Format("January 2, 2006"))
    fmt.Println(now.Format("Mon 3:04 PM"))
    fmt.Println(now.Format(time.RFC3339))
}
2026-04-18
2026-04-18 14:30:00
April 18, 2026
Sat 2:30 PM
2026-04-18T14:30:00-07:00

Parsing uses the same reference time.

t, err := time.Parse("2006-01-02", "2026-04-18")
if err != nil {
    log.Fatal(err)
}
fmt.Println(t)

The standard library provides predefined constants for common formats: time.RFC3339, time.RFC1123, time.Kitchen, and others.

Timers & Tickers

time.Timer fires once after a duration. time.Ticker fires repeatedly at an interval.

func main() {
    // Timer: fires once
    timer := time.NewTimer(2 * time.Second)
    fmt.Println("Waiting for timer...")
    <-timer.C
    fmt.Println("Timer fired!")

    // Ticker: fires repeatedly
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop()

    for i := 0; i < 5; i++ {
        <-ticker.C
        fmt.Println("Tick", i)
    }
}
Waiting for timer...
Timer fired!
Tick 0
Tick 1
Tick 2
Tick 3
Tick 4

Always stop tickers when you are done with them. A leaked ticker creates a goroutine leak.

For simple delays, time.After returns a channel that receives after a duration. It is convenient in select statements.

select {
case result := <-work:
    fmt.Println("Got result:", result)
case <-time.After(5 * time.Second):
    fmt.Println("Timed out")
}

context.Context

The context package provides a mechanism for cancellation, deadlines, and request-scoped values. Every long-running or I/O-bound function in a Go service should accept a context.Context as its first parameter.

func FetchData(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

context.Background & context.TODO

context.Background() returns an empty context. Use it at the top of your call chain: in main, in test setup, or at the entry point of an incoming request.

context.TODO() is a placeholder for when you are not sure which context to use. It signals to code reviewers that this needs to be revisited.

func main() {
    ctx := context.Background()
    data, err := FetchData(ctx, "https://api.example.com/data")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(data))
}

context.WithTimeout & context.WithDeadline

context.WithTimeout creates a context that cancels automatically after a duration.

func HandleRequest(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel() // always call cancel to release resources

    result, err := slowDatabaseQuery(ctx)
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            http.Error(w, "request timed out", http.StatusGatewayTimeout)
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(result)
}

context.WithDeadline is similar but takes an absolute time instead of a duration.

deadline := time.Now().Add(30 * time.Second)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()

context.WithCancel

context.WithCancel creates a context that can be cancelled manually. This is useful for stopping background work.

func StartWorker(ctx context.Context) {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    go func() {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("Worker stopped:", ctx.Err())
                return
            case <-time.After(1 * time.Second):
                fmt.Println("Working...")
            }
        }
    }()

    // Let the worker run for 5 seconds
    time.Sleep(5 * time.Second)
    cancel() // signal the worker to stop
    time.Sleep(100 * time.Millisecond) // let it print the stop message
}
Working...
Working...
Working...
Working...
Working...
Worker stopped: context canceled

Passing Context Through Your Call Chain

Context flows down through function calls. Each layer can add deadlines or cancellation.

func HandleOrder(ctx context.Context, orderID string) error {
    // Add a timeout for the entire operation
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()

    order, err := fetchOrder(ctx, orderID)
    if err != nil {
        return fmt.Errorf("fetch order: %w", err)
    }

    if err := validateOrder(ctx, order); err != nil {
        return fmt.Errorf("validate: %w", err)
    }

    if err := chargePayment(ctx, order); err != nil {
        return fmt.Errorf("payment: %w", err)
    }

    if err := sendConfirmation(ctx, order); err != nil {
        return fmt.Errorf("confirmation: %w", err)
    }

    return nil
}

If the timeout expires after chargePayment but before sendConfirmation, the context cancellation propagates and sendConfirmation can return early instead of hanging.

Checking for Cancellation

Long-running functions should check ctx.Done() periodically.

func ProcessBatch(ctx context.Context, items []Item) error {
    for i, item := range items {
        select {
        case <-ctx.Done():
            return fmt.Errorf("cancelled after processing %d/%d items: %w",
                i, len(items), ctx.Err())
        default:
        }

        if err := processItem(ctx, item); err != nil {
            return fmt.Errorf("item %d: %w", i, err)
        }
    }
    return nil
}

Never Store Context in a Struct

Context should be passed as a function parameter, not stored as a struct field.

// Wrong — context stored in a struct
type Service struct {
    ctx context.Context
    db  *sql.DB
}

// Correct — context passed per-call
type Service struct {
    db *sql.DB
}

func (s *Service) GetUser(ctx context.Context, id int) (User, error) {
    row := s.db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = $1", id)
    var u User
    err := row.Scan(&u.ID, &u.Name)
    return u, err
}

Storing context in a struct ties the context lifetime to the struct lifetime. Contexts should be request-scoped, created fresh for each operation.

context.WithValue

Context can carry request-scoped values like request IDs or authentication tokens. Use it sparingly.

type contextKey string

const requestIDKey contextKey = "requestID"

func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, requestIDKey, id)
}

func GetRequestID(ctx context.Context) string {
    id, ok := ctx.Value(requestIDKey).(string)
    if !ok {
        return "unknown"
    }
    return id
}

Use custom types for context keys to avoid collisions between packages. Never use context.WithValue for passing function arguments — it is for metadata that crosses API boundaries (tracing IDs, auth tokens).

Common Pitfalls

  • Forgetting to call cancel. Every context.WithTimeout, WithDeadline, and WithCancel returns a cancel function. Always defer cancel() to release resources.
  • Using the wrong reference time component. The reference time is 01/02 03:04:05 PM '06 -0700. Mixing up the values produces silently wrong results. Double-check against the reference.
  • Storing context in a struct. Contexts are request-scoped. Storing them in a long-lived struct means the context may be cancelled or expired when you try to use it.
  • Ignoring context cancellation. If your function accepts a context, respect its cancellation. Check ctx.Done() in loops and pass it to downstream calls.
  • Using time.Sleep instead of context. In production code, prefer context.WithTimeout and select over time.Sleep. Sleep cannot be cancelled.
  • Comparing times from different locations. time.Time values include location information. Use time.Time.Equal() for comparison instead of ==, which also compares the location.

Key Takeaways

  • time.Time represents instants; time.Duration represents spans. Use time.Now(), Add(), and Sub() for arithmetic.
  • Go formats time using a reference time (2006-01-02 15:04:05), not format codes.
  • Stop tickers when done; use time.After for one-shot timeouts in select statements.
  • context.Context carries cancellation, deadlines, and request-scoped values through your call chain.
  • Always pass context as the first function parameter, never store it in a struct.
  • Always defer cancel() when creating a derived context.
  • Use context.WithTimeout for I/O operations, context.WithCancel for background work, and context.WithValue sparingly for request metadata.