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, andWithCancelreturns a cancel function. Alwaysdefer 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.WithTimeoutandselectovertime.Sleep. Sleep cannot be cancelled. - Comparing times from different locations.
time.Timevalues include location information. Usetime.Time.Equal()for comparison instead of==, which also compares the location.
Key Takeaways
time.Timerepresents instants;time.Durationrepresents spans. Usetime.Now(),Add(), andSub()for arithmetic.- Go formats time using a reference time (
2006-01-02 15:04:05), not format codes. - Stop tickers when done; use
time.Afterfor one-shot timeouts inselectstatements. context.Contextcarries 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.WithTimeoutfor I/O operations,context.WithCancelfor background work, andcontext.WithValuesparingly for request metadata.