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:#f3e5f5time.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 time — Mon 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 --> Formatt := 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:#e8f5e9now := 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.Localuses 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.Localis usually UTC unless explicitly configured. Always usetime.LoadLocationwith an explicit zone name, ortime.UTCfor 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:#fff3e0time.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 callticker.Stop()after you’re done using a Ticker — usually withdefer ticker.Stop(). An unstopped ticker keeps running, and goroutines reading fromticker.Cwill 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
endimport (
"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.Timealways stores a time zone — twotime.Timevalues in different zones but the same moment will beEqualbut not==. Always use.Equal()for comparisons, not==.- Go formats use the reference time
Mon Jan 2 15:04:05 MST 2006— notYYYY-MM-DD. Memorize: 2006 (year), 01 (month), 02 (day), 15 (hour), 04 (minute), 05 (second).time.RFC3339is 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.ParseInLocationnottime.Parsewhen the input doesn’t include a time zone —time.Parseassumes UTC, which is often not what you want.defer ticker.Stop()is required aftertime.NewTicker— an unstopped Ticker is a slow but certain goroutine leak that drains memory.time.Since(t)is a shortcut fortime.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.WithTimeoutis better thantime.Afterfor production timeouts because the timeout propagates to all downstream context-aware operations.AddDatehandles month overflow automatically — January 31 + 1 month produces March 3, not an error or an invalid date.