Time #

Time is one of those concepts that seems simple but is full of traps in programming — different time zones, inconsistent date formats, daylight saving time, nanosecond precision, incorrect time comparisons, and inaccurate scheduling. The time package in Go handles all this complexity with a clean, consistent API. It provides the time.Time type to represent a specific moment, time.Duration for time intervals, parsing and formatting functions with a unique pattern, and Timer and Ticker for scheduling. Understanding the time package well isn’t just about printing dates — it’s about avoiding subtle time zone bugs, managing timeouts correctly, and building systems that behave deterministically with respect to time.

An Overview of the time Package #

flowchart TD
    T["package time"] --> Repr["Time Representation"]
    T --> Parse["Parsing & Formatting"]
    T --> Arith["Time Arithmetic"]
    T --> Sched["Scheduling"]
    T --> Zone["Time Zones"]

    Repr --> R1["time.Time — a specific moment"]
    Repr --> R2["time.Duration — a time interval"]
    Repr --> R3["time.Now() — the current time"]

    Parse --> P1["time.Parse — string → Time"]
    Parse --> P2["t.Format — Time → string"]
    Parse --> P3["time.RFC3339, time.Kitchen, etc."]

    Arith --> A1["t.Add(d) — add a duration"]
    Arith --> A2["t.Sub(t2) — difference of two times"]
    Arith --> A3["t.Before / t.After / t.Equal"]

    Sched --> S1["time.Sleep — delay execution"]
    Sched --> S2["time.After — a channel after a duration"]
    Sched --> S3["time.NewTimer — one-shot timer"]
    Sched --> S4["time.NewTicker — periodic ticker"]

    Zone --> Z1["time.LoadLocation — load a time zone"]
    Zone --> Z2["t.In(loc) — time zone conversion"]
    Zone --> Z3["time.UTC / time.Local"]

    style T fill:#4f86c6,color:#fff
    style Repr fill:#e8f5e9
    style Parse fill:#e3f2fd
    style Arith fill:#fff3e0
    style Sched fill:#fce4ec
    style Zone fill:#f3e5f5

time.Time — Representing Time #

time.Time is the main type of the time package. It represents a moment in time with nanosecond precision, and always stores time zone information along with its value.

package main

import (
    "fmt"
    "time"
)

func main() {
    // The current time — with the local time zone
    now := time.Now()
    fmt.Println(now)
    // 2024-03-15 14:30:00.123456789 +0700 WIB

    // Creating a time.Time from specific components
    birthday := time.Date(1990, time.March, 15, 0, 0, 0, 0, time.Local)
    fmt.Println(birthday)
    // 1990-03-15 00:00:00 +0700 WIB

    // Time in UTC
    nowUTC := time.Now().UTC()
    fmt.Println(nowUTC)
    // 2024-03-15 07:30:00.123456789 +0000 UTC

    // Zero value time.Time — January 1, year 1, 00:00:00 UTC
    var t time.Time
    fmt.Println(t)           // 0001-01-01 00:00:00 +0000 UTC
    fmt.Println(t.IsZero())  // true — useful for checking "not set"

    // Unix timestamp — seconds since January 1, 1970 UTC
    now = time.Now()
    fmt.Println(now.Unix())      // 1710491400
    fmt.Println(now.UnixMilli()) // 1710491400123 — milliseconds
    fmt.Println(now.UnixNano())  // 1710491400123456789 — nanoseconds

    // From a Unix timestamp
    fromUnix := time.Unix(1710491400, 0)
    fmt.Println(fromUnix)
    // 2024-03-15 14:30:00 +0700 WIB
}

Accessing Time Components #

t := time.Now()

// Date components
fmt.Println(t.Year())        // 2024
fmt.Println(t.Month())       // March (type time.Month)
fmt.Println(int(t.Month()))  // 3
fmt.Println(t.Day())         // 15
fmt.Println(t.Weekday())     // Friday (type time.Weekday)
fmt.Println(int(t.Weekday())) // 5 (0=Sunday, 6=Saturday)

// Time components
fmt.Println(t.Hour())        // 14
fmt.Println(t.Minute())      // 30
fmt.Println(t.Second())      // 0
fmt.Println(t.Nanosecond())  // 123456789

// Day of the year (1-365/366)
fmt.Println(t.YearDay())     // 75

// ISO 8601 week number
year, week := t.ISOWeek()
fmt.Printf("Year %d, Week %d\n", year, week)

// Date and Clock in one call
year2, month, day := t.Date()
hour, minute, second := t.Clock()
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n",
    year2, month, day, hour, minute, second)

Parsing and Formatting #

Go uses a unique approach to time formatting: instead of symbols like YYYY-MM-DD, Go uses a specific reference timeMon Jan 2 15:04:05 MST 2006. Every format component is represented by a value from this reference time.

flowchart LR
    subgraph Ref["Go Reference Time"]
        direction TB
        R1["2006 → year\n(the 6th in the sequence 1 2 3 4 5 6)"]
        R2["01 → month\n(01=January)"]
        R3["02 → day\n(02=the 2nd)"]
        R4["15 → 24h hour\n(15:00 = 3pm)"]
        R5["04 → minute\n(04)"]
        R6["05 → second\n(05)"]
        R7["MST → time zone\n(Mountain Standard Time)"]
    end

    subgraph Format["Format Examples"]
        direction TB
        F1["2006-01-02 → YYYY-MM-DD"]
        F2["02/01/2006 → DD/MM/YYYY"]
        F3["15:04:05 → HH:MM:SS"]
        F4["2006-01-02T15:04:05Z07:00 → RFC3339"]
        F5["Jan 2, 2006 → 'Mar 15, 2024'"]
    end

    Ref --> Format
t := time.Now()

// Format to a string
fmt.Println(t.Format("2006-01-02"))
// 2024-03-15

fmt.Println(t.Format("02/01/2006"))
// 15/03/2024

fmt.Println(t.Format("2006-01-02 15:04:05"))
// 2024-03-15 14:30:00

fmt.Println(t.Format("Monday, 02 January 2006"))
// Friday, 15 March 2024

fmt.Println(t.Format("15:04:05.000"))
// 14:30:00.123 — milliseconds

// Built-in format constants
fmt.Println(t.Format(time.RFC3339))
// 2024-03-15T14:30:00+07:00

fmt.Println(t.Format(time.RFC3339Nano))
// 2024-03-15T14:30:00.123456789+07:00

fmt.Println(t.Format(time.RFC1123))
// Fri, 15 Mar 2024 14:30:00 WIB

fmt.Println(t.Format(time.Kitchen))
// 2:30PM

fmt.Println(t.Format(time.DateOnly))   // Go 1.20+
// 2024-03-15

fmt.Println(t.Format(time.TimeOnly))   // Go 1.20+
// 14:30:00

Parsing a String into time.Time #

// time.Parse — parse with the time zone from the format string
t1, err := time.Parse("2006-01-02", "2024-03-15")
if err != nil {
    fmt.Println("parse error:", err)
    return
}
fmt.Println(t1) // 2024-03-15 00:00:00 +0000 UTC
// NOTE: the time zone is UTC if it's not in the format!

// time.ParseInLocation — parse with an explicit time zone
loc, _ := time.LoadLocation("Asia/Jakarta")
t2, err := time.ParseInLocation("2006-01-02 15:04:05",
    "2024-03-15 14:30:00", loc)
if err != nil {
    fmt.Println("parse error:", err)
    return
}
fmt.Println(t2) // 2024-03-15 14:30:00 +0700 WIB

// Parse RFC3339 — the format most recommended for APIs
t3, err := time.Parse(time.RFC3339, "2024-03-15T14:30:00+07:00")
fmt.Println(t3) // 2024-03-15 14:30:00 +0700 +0700

// ANTI-PATTERN: assuming the format without validation
func parseDay(s string) time.Time {
    t, _ := time.Parse("2006-01-02", s) // ignore the error — dangerous!
    return t // returns the zero value if parsing fails, without any warning
}

// CORRECT: always check the parsing error
func parseDayWell(s string) (time.Time, error) {
    t, err := time.Parse("2006-01-02", s)
    if err != nil {
        return time.Time{}, fmt.Errorf("parseDay %q: %w", s, err)
    }
    return t, nil
}

time.Duration — Representing Intervals #

time.Duration is an int64 representing a time interval in nanoseconds. Go provides constants for commonly used units.

// Duration constants
fmt.Println(time.Nanosecond)   // 1ns
fmt.Println(time.Microsecond)  // 1µs
fmt.Println(time.Millisecond)  // 1ms
fmt.Println(time.Second)       // 1s
fmt.Println(time.Minute)       // 1m0s
fmt.Println(time.Hour)         // 1h0m0s

// Creating durations
halfHour := 30 * time.Minute         // 30m0s
oneAndHalfHours := 90 * time.Minute  // 1h30m0s
threeDays := 3 * 24 * time.Hour      // 72h0m0s

// Converting a duration to numeric units
d := 2*time.Hour + 30*time.Minute + 15*time.Second
fmt.Println(d)                    // 2h30m15s
fmt.Println(d.Hours())            // 2.504166... (float64)
fmt.Println(d.Minutes())          // 150.25 (float64)
fmt.Println(d.Seconds())          // 9015 (float64)
fmt.Println(d.Milliseconds())     // 9015000 (int64)
fmt.Println(d.Nanoseconds())      // 9015000000000 (int64)

// Parsing a duration from a string
d2, err := time.ParseDuration("2h30m15s")
d3, err := time.ParseDuration("1.5h")
d4, err := time.ParseDuration("300ms")
d5, err := time.ParseDuration("2.5s")

// Rounding and truncation
d6 := 2*time.Hour + 37*time.Minute + 42*time.Second
fmt.Println(d6.Round(time.Minute))    // 2h38m0s — rounded to the minute
fmt.Println(d6.Truncate(time.Minute)) // 2h37m0s — truncated to the minute
fmt.Println(d6.Abs())                 // 2h37m42s — absolute value (Go 1.19+)

Time Arithmetic #

Arithmetic operations on time — adding, subtracting, and comparing — are done very often and are easy to get wrong if you’re not careful.

flowchart LR
    T1["time.Time\nt1"] 
    T2["time.Time\nt2"]
    D["time.Duration\nd"]

    T1 -- "t1.Add(d)" --> T3["time.Time\nt1 + d"]
    T1 -- "t1.Sub(t2)" --> D2["time.Duration\nt1 - t2"]
    T2 -- "t2.Add(-d)" --> T4["time.Time\nt2 - d"]
    T1 -- "t1.Before(t2)" --> B["bool"]
    T1 -- "t1.After(t2)" --> B
    T1 -- "t1.Equal(t2)" --> B

    style T1 fill:#e3f2fd
    style T2 fill:#e3f2fd
    style D fill:#fff3e0
    style D2 fill:#fff3e0
    style T3 fill:#e8f5e9
    style T4 fill:#e8f5e9
now := time.Now()

// Add — add a duration to a time
tomorrow := now.Add(24 * time.Hour)
inOneHour := now.Add(time.Hour)
fiveMinutesAgo := now.Add(-5 * time.Minute)

// AddDate — add years, months, days (more natural for calendars)
nextMonth := now.AddDate(0, 1, 0)   // +1 month
nextYear := now.AddDate(1, 0, 0)    // +1 year
aWeekAgo := now.AddDate(0, 0, -7)   // -7 days

// NOTE: AddDate handles month overflow automatically
// January 31 + 1 month = March 3 (not February 31)
endOfJan := time.Date(2024, time.January, 31, 0, 0, 0, 0, time.UTC)
fmt.Println(endOfJan.AddDate(0, 1, 0))
// 2024-03-02 00:00:00 +0000 UTC (not 2024-02-31!)

// Sub — the difference between two times, producing a Duration
start := time.Now()
time.Sleep(100 * time.Millisecond)
end := time.Now()
elapsed := end.Sub(start)
fmt.Printf("Execution time: %v\n", elapsed) // 100.123ms

// Since and Until — very common shortcuts
fmt.Println(time.Since(start))   // equivalent to time.Now().Sub(start)
fmt.Println(time.Until(tomorrow))   // equivalent to tomorrow.Sub(time.Now())

// Time comparisons
t1 := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC)

fmt.Println(t1.Before(t2)) // true
fmt.Println(t1.After(t2))  // false
fmt.Println(t1.Equal(t2))  // false

// ANTI-PATTERN: comparing with == — doesn't account for time zones
tJakarta := time.Date(2024, 1, 1, 7, 0, 0, 0, loc) // 07:00 WIB
tUTC := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) // 00:00 UTC
fmt.Println(tJakarta == tUTC)        // false! even though they're the same moment
fmt.Println(tJakarta.Equal(tUTC))    // true  — this is correct

Time Zones #

Time zones are the source of the most commonly undetected bugs in time-related systems. The basic principle is simple: always store and transfer time in UTC, and only convert to the local zone when displaying to users.

flowchart TD
    subgraph Principle["Time Zone Principle"]
        direction LR
        P1["Store in DB\nin UTC"]
        P2["Transfer via API\nin RFC3339 + offset"]
        P3["Display to users\nin the local zone"]
        P1 --> P2 --> P3
    end

    subgraph Go["In Go"]
        direction TB
        G1["time.Now().UTC()\nfor internal operations"]
        G2["t.In(loc)\nfor display conversion"]
        G3["time.LoadLocation('Asia/Jakarta')\nfor the Indonesia zone"]
    end

    Principle --> Go
// Load a time zone from the IANA database
// Needs tzdata — available on all modern OSes
loc, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
    fmt.Fprintf(os.Stderr, "failed to load timezone: %v\n", err)
    return
}

// Other zones common in Indonesia
locWITA, _ := time.LoadLocation("Asia/Makassar") // WIB+1
locWIT, _ := time.LoadLocation("Asia/Jayapura")  // WIB+2

// Fixed zones (fixed offset) — not affected by DST
wib := time.FixedZone("WIB", 7*60*60)   // UTC+7
wita := time.FixedZone("WITA", 8*60*60) // UTC+8
wit := time.FixedZone("WIT", 9*60*60)   // UTC+9

// Converting between zones
now := time.Now().UTC()
fmt.Println("UTC  :", now.Format("15:04:05 MST"))
fmt.Println("WIB  :", now.In(loc).Format("15:04:05 MST"))
fmt.Println("Tokyo:", now.In(mustLoadLoc("Asia/Tokyo")).Format("15:04:05 MST"))
fmt.Println("NYC  :", now.In(mustLoadLoc("America/New_York")).Format("15:04:05 MST"))

func mustLoadLoc(name string) *time.Location {
    loc, err := time.LoadLocation(name)
    if err != nil {
        panic(err)
    }
    return loc
}

// Creating a time.Time in a specific zone
deadline := time.Date(2024, 3, 15, 17, 0, 0, 0, loc) // 17:00 WIB
fmt.Println(deadline.UTC()) // 2024-03-15 10:00:00 +0000 UTC
time.Local uses the operating system’s time zone running the program — this can differ between a developer’s machine and a production server. In Linux-based containers, time.Local is usually UTC unless explicitly configured. Always use time.LoadLocation with an explicit zone name, or time.UTC for internal operations.

Handling DST (Daylight Saving Time) #

Indonesia doesn’t observe DST, but systems interacting with other time zones need to be careful:

// DST causes "missing hours" and "duplicate hours"
nyLoc, _ := time.LoadLocation("America/New_York")

// "Missing hour" — 2:30 AM doesn't exist during spring forward
// Go handles this automatically by adjusting to a valid hour
missingHour := time.Date(2024, 3, 10, 2, 30, 0, 0, nyLoc)
fmt.Println(missingHour) // automatically adjusted

// Calculate a time difference across DST — use Sub, not AddDate
beforeDST := time.Date(2024, 3, 9, 12, 0, 0, 0, nyLoc)
afterDST := time.Date(2024, 3, 11, 12, 0, 0, 0, nyLoc)
// The difference is 47 hours, not 48!
fmt.Println(afterDST.Sub(beforeDST)) // 47h0m0s

Timers and Tickers — Scheduling #

time.Timer and time.Ticker are mechanisms for executing something in the future or periodically. Both use a channel as the signal.

flowchart TD
    subgraph Timer["time.Timer — One-shot"]
        T1["time.NewTimer(d)"] --> T2["Wait..."]
        T2 --> T3["<-timer.C\nreceive once after d"]
        T1 --> T4["timer.Stop()\ncancel before it fires"]
        T1 --> T5["timer.Reset(d)\nreset the duration"]
    end

    subgraph Ticker["time.Ticker — Periodic"]
        K1["time.NewTicker(d)"] --> K2["Tick..."]
        K2 --> K3["<-ticker.C\nreceive every d"]
        K3 --> K2
        K1 --> K4["ticker.Stop()\nstop the ticker"]
    end

    subgraph After["Shortcuts"]
        A1["time.After(d)\nreturns <-chan Time"]
        A2["time.Sleep(d)\nblocks the goroutine"]
        A3["time.AfterFunc(d, f)\nruns f after d"]
    end

    style Timer fill:#e8f5e9
    style Ticker fill:#e3f2fd
    style After fill:#fff3e0

time.Sleep — Delaying Execution #

// Delay the current goroutine's execution
fmt.Println("Starting...")
time.Sleep(2 * time.Second)
fmt.Println("2 seconds have passed")

// Sleep with a duration from a string
duration, _ := time.ParseDuration("500ms")
time.Sleep(duration)

time.Timer — One-shot Timers #

// A timer that fires once after a duration
timer := time.NewTimer(3 * time.Second)

fmt.Println("Waiting for the timer...")
<-timer.C // block until the timer fires
fmt.Println("Timer fired!")

// Canceling a timer before it fires
timer2 := time.NewTimer(10 * time.Second)
go func() {
    time.Sleep(2 * time.Second)
    // Stop the timer — returns true if it was successfully stopped
    if timer2.Stop() {
        fmt.Println("Timer canceled")
    }
}()

select {
case <-timer2.C:
    fmt.Println("Timer fired")
case <-time.After(5 * time.Second):
    fmt.Println("Timed out waiting for the timer")
}

// ANTI-PATTERN: Reset without draining the channel first
timer3 := time.NewTimer(time.Second)
timer3.Stop()
timer3.Reset(2 * time.Second) // there may be a value in the channel!

// CORRECT: Stop then drain before Reset
timer4 := time.NewTimer(time.Second)
if !timer4.Stop() {
    <-timer4.C // drain the channel if it already fired
}
timer4.Reset(2 * time.Second)

time.Ticker — Periodic Execution #

// A ticker that ticks every interval
ticker := time.NewTicker(time.Second)
defer ticker.Stop() // REQUIRED: stop the ticker when done, avoid a goroutine leak

limit := time.After(5 * time.Second)

for {
    select {
    case t := <-ticker.C:
        fmt.Println("Tick:", t.Format("15:04:05"))
    case <-limit:
        fmt.Println("Done")
        return
    }
}
// Output:
// Tick: 14:30:01
// Tick: 14:30:02
// Tick: 14:30:03
// Tick: 14:30:04
// Done
Always call ticker.Stop() after you’re done using a Ticker — usually with defer ticker.Stop(). An unstopped ticker keeps running, and goroutines reading from ticker.C will never finish (goroutine leak). This is one of the most common sources of memory leaks in Go programs using Tickers.

time.AfterFunc — Asynchronous Execution #

// Run a function in a new goroutine after a duration
timer := time.AfterFunc(5*time.Second, func() {
    fmt.Println("This runs in a separate goroutine after 5 seconds!")
    // Be careful with shared state access — synchronization is needed
})

// Cancel if needed
timer.Stop()

// Pattern: retry with exponential backoff
func retryWithBackoff(fn func() error, maxRetry int) error {
    var err error
    for i := 0; i < maxRetry; i++ {
        if err = fn(); err == nil {
            return nil
        }
        if i < maxRetry-1 {
            backoff := time.Duration(1<<uint(i)) * 100 * time.Millisecond
            // cap the backoff at 30 seconds
            if backoff > 30*time.Second {
                backoff = 30 * time.Second
            }
            fmt.Printf("Retry %d/%d after %v: %v\n", i+1, maxRetry, backoff, err)
            time.Sleep(backoff)
        }
    }
    return fmt.Errorf("failed after %d retries: %w", maxRetry, err)
}

Timeouts with time.After and context #

Timeouts are a very common pattern in Go applications — limiting how long an operation may run. There are two main ways: time.After for simple cases, and context.WithTimeout for better integration with the Go stack.

sequenceDiagram
    participant Main as Main Goroutine
    participant Op as Operation (DB, HTTP, etc.)
    participant Timer as time.After / context

    Main->>Op: Start the operation
    Main->>Timer: Set a timeout (e.g. 5 seconds)

    alt Operation completes in time
        Op-->>Main: Result
        Main->>Timer: Stop the timer (if Timer)
    else Timeout comes first
        Timer-->>Main: Timeout signal
        Main->>Op: Cancel / ignore the late result
        Main-->>Main: Return a timeout error
    end
import (
    "context"
    "fmt"
    "time"
)

// Pattern 1: time.After — for simple goroutines
func operationWithTimeout(duration time.Duration) error {
    result := make(chan string, 1)

    go func() {
        // Simulate a time-consuming operation
        time.Sleep(3 * time.Second)
        result <- "done"
    }()

    select {
    case r := <-result:
        fmt.Println("Success:", r)
        return nil
    case <-time.After(duration):
        return fmt.Errorf("operation timed out after %v", duration)
    }
}

// Pattern 2: context.WithTimeout — the recommended way for production
func queryWithTimeout(ctx context.Context, id int) (string, error) {
    // Create a new context with a 5-second timeout
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel() // REQUIRED: always cancel to release resources

    // All context-aware operations (DB, HTTP client, etc.)
    // are automatically canceled when the context times out
    result := make(chan string, 1)
    go func() {
        // Simulate a DB query
        time.Sleep(2 * time.Second)
        result <- fmt.Sprintf("data for id %d", id)
    }()

    select {
    case r := <-result:
        return r, nil
    case <-ctx.Done():
        return "", fmt.Errorf("queryWithTimeout: %w", ctx.Err())
    }
}

// Example usage
func main() {
    ctx := context.Background()

    data, err := queryWithTimeout(ctx, 42)
    if err != nil {
        fmt.Fprintf(os.Stderr, "error: %v\n", err)
        return
    }
    fmt.Println(data)
}

Measuring Execution Time #

Measuring how long a function or block of code runs is a common need for profiling and optimization.

// Pattern 1: manual with time.Now and time.Since
func measureTime(name string, fn func()) {
    start := time.Now()
    fn()
    fmt.Printf("%s finished in %v\n", name, time.Since(start))
}

// Pattern 2: defer for automatic timing
func processData(data []int) {
    defer func(start time.Time) {
        fmt.Printf("processData(%d items) took %v\n",
            len(data), time.Since(start))
    }(time.Now()) // time.Now() is evaluated when the defer is declared!

    // ... processing logic
    time.Sleep(100 * time.Millisecond)
}

// Pattern 3: for benchmarks in tests
func BenchmarkProcess(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // The function being measured
        processSomething()
    }
}

// Example manual timing
start := time.Now()

for i := 0; i < 1000000; i++ {
    // operation
}

elapsed := time.Since(start)
fmt.Printf("1 million iterations: %v (%.2f ns/op)\n",
    elapsed,
    float64(elapsed.Nanoseconds())/1000000)

Production Usage Patterns #

A Simple Scheduler #

// Run a task every day at 02:00
func scheduleDaily(hour, minute int, task func()) {
    for {
        now := time.Now()
        // Calculate the next time
        next := time.Date(
            now.Year(), now.Month(), now.Day(),
            hour, minute, 0, 0, now.Location(),
        )

        // If today's target hour has passed, schedule for tomorrow
        if next.Before(now) {
            next = next.Add(24 * time.Hour)
        }

        diff := time.Until(next)
        fmt.Printf("Next task in %v (at %02d:%02d)\n",
            diff.Round(time.Minute), hour, minute)

        <-time.After(diff)
        go task() // run in a goroutine so the scheduler isn't blocked
    }
}

A Simple Rate Limiter with a Ticker #

// Limit execution to N operations per second
func makeRateLimiter(opsPerSecond int) <-chan time.Time {
    return time.NewTicker(time.Second / time.Duration(opsPerSecond)).C
}

func main() {
    // At most 5 requests per second
    rateLimiter := makeRateLimiter(5)

    requests := []string{"req1", "req2", "req3", "req4", "req5", "req6", "req7"}

    for _, req := range requests {
        <-rateLimiter // wait for the turn
        go processRequest(req)
    }
}

A Cache with Expiry #

import (
    "sync"
    "time"
)

type CacheItem struct {
    Value     any
    ExpiresAt time.Time
}

type Cache struct {
    mu   sync.RWMutex
    data map[string]CacheItem
}

func NewCache() *Cache {
    c := &Cache{data: make(map[string]CacheItem)}
    // Clean expired items every minute
    go c.cleanupPeriodically()
    return c
}

func (c *Cache) Set(key string, value any, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = CacheItem{
        Value:     value,
        ExpiresAt: time.Now().Add(ttl),
    }
}

func (c *Cache) Get(key string) (any, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    item, exists := c.data[key]
    if !exists {
        return nil, false
    }
    // Check whether it has expired
    if time.Now().After(item.ExpiresAt) {
        return nil, false
    }
    return item.Value, true
}

func (c *Cache) cleanupPeriodically() {
    ticker := time.NewTicker(time.Minute)
    defer ticker.Stop()

    for range ticker.C {
        c.mu.Lock()
        now := time.Now()
        for key, item := range c.data {
            if now.After(item.ExpiresAt) {
                delete(c.data, key)
            }
        }
        c.mu.Unlock()
    }
}

Measuring the Age of Something #

// Calculate an age in human terms
func humanAge(t time.Time) string {
    diff := time.Since(t)

    switch {
    case diff < time.Minute:
        return "just now"
    case diff < time.Hour:
        minutes := int(diff.Minutes())
        return fmt.Sprintf("%d minutes ago", minutes)
    case diff < 24*time.Hour:
        hours := int(diff.Hours())
        return fmt.Sprintf("%d hours ago", hours)
    case diff < 7*24*time.Hour:
        days := int(diff.Hours() / 24)
        return fmt.Sprintf("%d days ago", days)
    case diff < 30*24*time.Hour:
        weeks := int(diff.Hours() / 24 / 7)
        return fmt.Sprintf("%d weeks ago", weeks)
    case diff < 365*24*time.Hour:
        months := int(diff.Hours() / 24 / 30)
        return fmt.Sprintf("%d months ago", months)
    default:
        years := int(diff.Hours() / 24 / 365)
        return fmt.Sprintf("%d years ago", years)
    }
}

// Example usage
postTime := time.Now().Add(-3 * time.Hour)
fmt.Println(humanAge(postTime)) // 3 hours ago

postTime2 := time.Now().Add(-2 * 24 * time.Hour)
fmt.Println(humanAge(postTime2)) // 2 days ago

When to Switch to Alternatives #

Keep using time if:
  ✓ All basic date, time, and duration operations
  ✓ Parsing and formatting time in various formats
  ✓ Periodic scheduling with Ticker and one-shot with Timer
  ✓ Timeouts and deadlines with time.After or context.WithTimeout
  ✓ Time zone conversions with LoadLocation

Consider external libraries if:
  ✗ Complex calendar calculations (Hijri, Javanese calendars, etc.)
     → there's no standard library for these
  ✗ Parsing dates from various unknown formats
     → dateparse (github.com/araddon/dateparse)
  ✗ Human-readable duration parsing ("in 2 hours", "yesterday")
     → naturaldate or similar libraries
  ✗ Cron expression scheduling ("0 2 * * *")
     → robfig/cron
  ✗ Business calendar manipulation (working days, holidays)
     → not in the stdlib, needs custom logic or a library

Summary #

  • time.Time always stores a time zone — two time.Time values in different zones but the same moment will be Equal but not ==. Always use .Equal() for comparisons, not ==.
  • Go formats use the reference time Mon Jan 2 15:04:05 MST 2006 — not YYYY-MM-DD. Memorize: 2006 (year), 01 (month), 02 (day), 15 (hour), 04 (minute), 05 (second).
  • time.RFC3339 is the best format for APIs — use it when serializing time to JSON or HTTP responses so any system can easily parse it.
  • Always check the error from time.Parse — a failed parse returns the zero value without panicking, causing hidden bugs that are hard to trace.
  • Use time.ParseInLocation not time.Parse when the input doesn’t include a time zone — time.Parse assumes UTC, which is often not what you want.
  • defer ticker.Stop() is required after time.NewTicker — an unstopped Ticker is a slow but certain goroutine leak that drains memory.
  • time.Since(t) is a shortcut for time.Now().Sub(t) — use it to measure elapsed time concisely.
  • Store time in UTC in the database and transfer via API — convert to the local zone only when displaying to users.
  • context.WithTimeout is better than time.After for production timeouts because the timeout propagates to all downstream context-aware operations.
  • AddDate handles month overflow automatically — January 31 + 1 month produces March 3, not an error or an invalid date.

← Previous: Os   Next: Strconv →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact