Date & Time #

The time package in Go holds a quirk that surprises almost every new developer: time formatting doesn’t use YYYY-MM-DD HH:mm:ss like most other languages, but rather a reference time — a specific date and time used as the template. Why 2006-01-02 and not YYYY-MM-DD? Because Go’s designers wanted a format that reads as its own example output, not abstract symbols you have to memorize. Beyond this quirk, Go’s time package is very rich: an expressive Duration type, explicit timezone handling, and Timer/Ticker primitives for scheduling.

time.Time — The Basic Type #

time.Time is the primary type representing a single point in time with nanosecond precision. It stores timezone information internally:

import "time"

// Zero value — January 1 of 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

// Check whether a time has been set
func processOrder(order Order) {
    if order.PaidAt.IsZero() {
        fmt.Println("Order not paid")
        return
    }
    fmt.Println("Paid at:", order.PaidAt)
}

Creating Times #

The Current Time #

now := time.Now()          // the system's local time
fmt.Println(now)            // 2024-07-28 15:30:45.123456789 +0700 WIB

nowUTC := time.Now().UTC() // the current time in UTC
fmt.Println(nowUTC)         // 2024-07-28 08:30:45.123456789 +0000 UTC

A Specific Time #

// time.Date(year, month, day, hour, min, sec, nanosec, location)
independenceDay := time.Date(2024, time.August, 17, 0, 0, 0, 0, time.UTC)
fmt.Println(independenceDay)  // 2024-08-17 00:00:00 +0000 UTC

// Use the time.Month constants for clarity
t := time.Date(2024, time.December, 31, 23, 59, 59, 0, time.Local)

From a Unix Timestamp #

// Unix timestamp — seconds since January 1, 1970 UTC
ts := int64(1722157845)
t := time.Unix(ts, 0)
fmt.Println(t)  // 2024-07-28 15:30:45 +0700 WIB

// Unix timestamp in milliseconds (common from JavaScript/APIs)
tsMs := int64(1722157845000)
t2 := time.UnixMilli(tsMs)
fmt.Println(t2)

// Convert back to a timestamp
fmt.Println(t.Unix())      // seconds
fmt.Println(t.UnixMilli()) // milliseconds
fmt.Println(t.UnixNano())  // nanoseconds

Go’s Unique Format System — The Reference Time #

This is both the most confusing and the most elegant thing about Go’s time package. Instead of abstract symbols like YYYY for year or HH for hour, Go uses a specific reference date and time:

Monday, January 2, 2006, 15:04:05, -0700

In numbers: 01/02 03:04:05PM '06 -0700

This reference date was chosen because each of its components represents a sequential number from 1 to 7 in a logical way, as visualized in the following diagram:

flowchart LR
    subgraph RefSequence["Go Reference Time Number Sequence"]
        direction LR
        Num1["1 (Month)\n01 / Jan / January"]
        Num2["2 (Day)\n02 / Mon / Monday"]
        Num3["3 (Hour)\n03 / 15"]
        Num4["4 (Minute)\n04"]
        Num5["5 (Second)\n05"]
        Num6["6 (Year)\n06 / 2006"]
        Num7["7 (Timezone)\n-0700 / MST"]
    end
    
    Num1 --> Num2 --> Num3 --> Num4 --> Num5 --> Num6 --> Num7

Each component represents:

  • 01 (1) = month (January is the 1st month)
  • 02 (2) = day / date
  • 15 (3) = hour (24-hour format, or 03 for the 12-hour format)
  • 04 (4) = minute
  • 05 (5) = second
  • 2006 (6) = year (or 06 for a 2-digit year)
  • -0700 (7) = timezone offset (or MST for the timezone name)
now := time.Now()

// Format various styles using the reference time
fmt.Println(now.Format("2006-01-02"))                    // 2024-07-28
fmt.Println(now.Format("02/01/2006"))                    // 28/07/2024
fmt.Println(now.Format("2006-01-02 15:04:05"))           // 2024-07-28 15:30:45
fmt.Println(now.Format("Monday, 02 January 2006"))       // Sunday, 28 July 2024
fmt.Println(now.Format("Jan 2, 2006 at 3:04pm"))         // Jul 28, 2024 at 3:30pm
fmt.Println(now.Format("15:04:05.000"))                  // 15:30:45.123 (milliseconds)
fmt.Println(now.Format("2006-01-02T15:04:05Z07:00"))     // ISO 8601 / RFC3339

Ready-to-Use Format Constants #

The time package provides commonly used format constants — no need to remember the formats yourself:

now := time.Now()

fmt.Println(now.Format(time.RFC3339))        // 2024-07-28T15:30:45+07:00
fmt.Println(now.Format(time.RFC3339Nano))    // 2024-07-28T15:30:45.123456789+07:00
fmt.Println(now.Format(time.RFC822))         // 28 Jul 24 15:30 WIB
fmt.Println(now.Format(time.RFC1123))        // Sun, 28 Jul 2024 15:30:45 WIB
fmt.Println(now.Format(time.Kitchen))        // 3:30PM
fmt.Println(now.Format(time.DateTime))       // 2024-07-28 15:30:45 (Go 1.20+)
fmt.Println(now.Format(time.DateOnly))       // 2024-07-28 (Go 1.20+)
fmt.Println(now.Format(time.TimeOnly))       // 15:30:45 (Go 1.20+)

Parsing Strings into time.Time #

time.Parse — Parsing Without a Timezone #

// Parse using the same layout as Format
layout := "2006-01-02"
t, err := time.Parse(layout, "2024-07-28")
if err != nil {
    log.Fatal(err)
}
fmt.Println(t)  // 2024-07-28 00:00:00 +0000 UTC

// Parse with a full time
layout2 := "2006-01-02 15:04:05"
t2, err := time.Parse(layout2, "2024-07-28 15:30:45")
if err != nil {
    log.Fatal(err)
}
fmt.Println(t2)  // 2024-07-28 15:30:45 +0000 UTC

// Parse RFC3339 (the most common API format)
t3, err := time.Parse(time.RFC3339, "2024-07-28T15:30:45+07:00")

time.ParseInLocation — Parsing with an Explicit Timezone #

// ANTI-PATTERN: time.Parse without timezone info produces UTC
layout := "2006-01-02 15:04:05"
t, _ := time.Parse(layout, "2024-07-28 15:30:45")
fmt.Println(t.Location())  // UTC — not WIB!

// CORRECT: use ParseInLocation when the string has no timezone info
wib, _ := time.LoadLocation("Asia/Jakarta")
t2, err := time.ParseInLocation(layout, "2024-07-28 15:30:45", wib)
if err != nil {
    log.Fatal(err)
}
fmt.Println(t2.Location())  // Asia/Jakarta ✓
fmt.Println(t2.UTC())       // 2024-07-28 08:30:45 +0000 UTC (minus 7 hours)
time.Parse always assumes UTC if the string contains no timezone information. For input from users or databases without a timezone marker, always use time.ParseInLocation with the correct timezone. This is a very common bug source in applications operating in non-UTC timezones.

Time Components #

t := time.Now()

// Individual components
fmt.Println(t.Year())        // 2024
fmt.Println(t.Month())       // July (type time.Month)
fmt.Println(int(t.Month()))  // 7
fmt.Println(t.Day())         // 28
fmt.Println(t.Hour())        // 15
fmt.Println(t.Minute())      // 30
fmt.Println(t.Second())      // 45
fmt.Println(t.Nanosecond())  // 123456789
fmt.Println(t.Weekday())     // Sunday (type time.Weekday)

// Combined components
year, month, day := t.Date()           // 2024, July, 28
hour, min, sec := t.Clock()            // 15, 30, 45

// Day and month names
fmt.Println(t.Weekday().String())  // "Sunday"
fmt.Println(t.Month().String())    // "July"

// Day of year
fmt.Println(t.YearDay())  // 210 (the 210th day of the year)

// Unix timestamps
fmt.Println(t.Unix())      // 1722157845
fmt.Println(t.UnixMilli()) // 1722157845123
fmt.Println(t.UnixNano())  // 1722157845123456789

time.Duration — Representing Durations #

time.Duration is an int64 type representing a duration in nanoseconds. The time package provides very expressive constants:

// 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
timeout := 30 * time.Second    // 30s
delay := 500 * time.Millisecond // 500ms
ttl := 24 * time.Hour          // 24h0m0s
week := 7 * 24 * time.Hour     // 168h0m0s

// Parsing a duration from a string
d, err := time.ParseDuration("1h30m45s")
if err != nil {
    log.Fatal(err)
}
fmt.Println(d)             // 1h30m45s
fmt.Println(d.Hours())     // 1.5125
fmt.Println(d.Minutes())   // 90.75
fmt.Println(d.Seconds())   // 5445
fmt.Println(d.Milliseconds()) // 5445000

// Duration arithmetic
fmt.Println(2*time.Hour + 30*time.Minute)  // 2h30m0s

Time Operations #

Adding and Subtracting Durations #

now := time.Now()

// Add — add a duration (can be negative to subtract)
tomorrow := now.Add(24 * time.Hour)
yesterday := now.Add(-24 * time.Hour)
inThreeHours := now.Add(3 * time.Hour)

// AddDate — add years, months, days
nextMonth := now.AddDate(0, 1, 0)   // add 1 month
nextYear := now.AddDate(1, 0, 0)   // add 1 year
inTwoWeeks := now.AddDate(0, 0, 14) // add 14 days

Calculating Differences #

start := time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2024, time.July, 28, 0, 0, 0, 0, time.UTC)

duration := end.Sub(start)
fmt.Println(duration)             // 5136h0m0s
fmt.Printf("%.0f days\n", duration.Hours()/24)  // 214 days

// time.Since — the difference from a past time to now
elapsed := time.Since(start)

// time.Until — the difference from now to a future time
remaining := time.Until(end)

Comparing Times #

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 — t1 is before t2
fmt.Println(t1.After(t2))   // false
fmt.Println(t1.Equal(t2))   // false

// Don't use == to compare time.Time!
// == doesn't account for timezones
// Use .Equal() which is timezone-aware
utc := time.Date(2024, 1, 1, 8, 0, 0, 0, time.UTC)
wib, _ := time.LoadLocation("Asia/Jakarta")
wibTime := time.Date(2024, 1, 1, 15, 0, 0, 0, wib)

fmt.Println(utc == wibTime)       // false — different representations
fmt.Println(utc.Equal(wibTime))   // true — the same moment

Truncate and Round #

t := time.Date(2024, 7, 28, 15, 37, 45, 123456789, time.UTC)

// Truncate — round down
fmt.Println(t.Truncate(time.Hour))          // 2024-07-28 15:00:00 UTC
fmt.Println(t.Truncate(time.Minute))        // 2024-07-28 15:37:00 UTC
fmt.Println(t.Truncate(24 * time.Hour))     // 2024-07-28 00:00:00 UTC

// Round — round to the nearest
fmt.Println(t.Round(time.Hour))             // 2024-07-28 16:00:00 UTC (37m > 30m)
fmt.Println(t.Round(time.Minute))           // 2024-07-28 15:38:00 UTC (45s > 30s)

// Useful for: ignoring millisecond precision when saving to a DB
createdAt := time.Now().Truncate(time.Second)

Timezones #

Built-in Timezone Constants #

// UTC — always available
t := time.Now().UTC()

// Local — the operating system's timezone
tLocal := time.Now().In(time.Local)
fmt.Println(time.Local)  // the local timezone name

LoadLocation — Timezone by Name #

// Timezone names follow the IANA Time Zone Database
wib, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
    log.Fatal(err)
}

wita, _ := time.LoadLocation("Asia/Makassar")
wit, _  := time.LoadLocation("Asia/Jayapura")
tokyo, _ := time.LoadLocation("Asia/Tokyo")
london, _ := time.LoadLocation("Europe/London")
newYork, _ := time.LoadLocation("America/New_York")

now := time.Now()
fmt.Println("WIB:    ", now.In(wib))
fmt.Println("WITA:   ", now.In(wita))
fmt.Println("WIT:    ", now.In(wit))
fmt.Println("Tokyo:  ", now.In(tokyo))
fmt.Println("London: ", now.In(london))
fmt.Println("NY:     ", now.In(newYork))

FixedZone — Manual Timezones #

// Create a timezone with a manual offset (without the IANA database)
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

t := time.Now().In(wib)
fmt.Println(t.Format("2006-01-02 15:04:05 MST"))
Always store times in UTC in the database. Conversion to a local timezone is only done when displaying to the user. This prevents various problems when the server changes timezones, daylight saving time changes, or the application serves users from various timezones.

Timers and Tickers #

time.Sleep — Delay Execution #

fmt.Println("Start")
time.Sleep(2 * time.Second)  // block for 2 seconds
fmt.Println("After 2 seconds")

time.After — A Channel That Receives After a Duration #

// Useful for timeouts
select {
case result := <-doWork():
    fmt.Println("Done:", result)
case <-time.After(5 * time.Second):
    fmt.Println("Timeout! The operation took too long")
}

time.NewTimer — A Cancellable Timer #

timer := time.NewTimer(3 * time.Second)
defer timer.Stop()  // important: always stop to prevent goroutine leaks

select {
case <-timer.C:
    fmt.Println("Timer fired!")
case <-cancel:
    fmt.Println("Cancelled before the timer")
}

time.NewTicker — Periodic Execution #

// Run every 1 second, stop after 5 ticks
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()  // always stop

count := 0
for range ticker.C {
    count++
    fmt.Printf("Tick %d: %s\n", count, time.Now().Format(time.TimeOnly))
    if count >= 5 {
        break
    }
}

// Common pattern: a ticker in a goroutine with a stop channel
stopCh := make(chan struct{})
go func() {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            doPeriodicWork()
        case <-stopCh:
            return
        }
    }
}()

Measuring Execution Duration #

// The manual way
start := time.Now()
doHeavyWork()
elapsed := time.Since(start)
fmt.Printf("Finished in %v\n", elapsed)

// With defer — elegant for profiling functions
func processLargeFile(path string) error {
    start := time.Now()
    defer func() {
        fmt.Printf("processLargeFile(%q) finished in %v\n",
            path, time.Since(start))
    }()

    // ... process the file
    return nil
}

// A simple benchmark
func benchmark(name string, fn func()) time.Duration {
    start := time.Now()
    fn()
    d := time.Since(start)
    fmt.Printf("[BENCH] %s: %v\n", name, d)
    return d
}

benchmark("sortLargeSlice", func() {
    sort.Ints(bigSlice)
})

Complete Example Program #

The following program builds a schedule management and performance measurement system:

package main

import (
    "fmt"
    "sort"
    "time"
)

// ── Scheduler ─────────────────────────────────────────────────

type Priority int

const (
    PriorityLow Priority = iota
    PriorityNormal
    PriorityHigh
    PriorityCritical
)

type Task struct {
    ID          int
    Name        string
    ScheduledAt time.Time
    Deadline    time.Time
    Priority    Priority
    Duration    time.Duration
    CompletedAt time.Time
}

func (t Task) IsOverdue() bool {
    if t.CompletedAt.IsZero() {
        return time.Now().After(t.Deadline)
    }
    return t.CompletedAt.After(t.Deadline)
}

func (t Task) TimeUntilDeadline() time.Duration {
    return time.Until(t.Deadline)
}

func (t Task) Status() string {
    if !t.CompletedAt.IsZero() {
        if t.IsOverdue() {
            return "✗ Completed (late)"
        }
        return "✓ Completed"
    }
    if t.IsOverdue() {
        return "⚠ Overdue"
    }
    remaining := t.TimeUntilDeadline()
    if remaining < time.Hour {
        return fmt.Sprintf("⚡ Urgent (%v left)", remaining.Round(time.Minute))
    }
    return fmt.Sprintf("○ Pending (%v left)", remaining.Round(time.Hour))
}

type Scheduler struct {
    tasks  []Task
    nextID int
    wib    *time.Location
}

func NewScheduler() *Scheduler {
    wib, _ := time.LoadLocation("Asia/Jakarta")
    return &Scheduler{wib: wib}
}

func (s *Scheduler) AddTask(name string, scheduledAt, deadline time.Time,
    priority Priority, duration time.Duration) Task {
    s.nextID++
    task := Task{
        ID:          s.nextID,
        Name:        name,
        ScheduledAt: scheduledAt,
        Deadline:    deadline,
        Priority:    priority,
        Duration:    duration,
    }
    s.tasks = append(s.tasks, task)
    return task
}

func (s *Scheduler) Complete(id int) error {
    for i := range s.tasks {
        if s.tasks[i].ID == id {
            s.tasks[i].CompletedAt = time.Now()
            return nil
        }
    }
    return fmt.Errorf("task ID %d not found", id)
}

func (s *Scheduler) TasksDueToday() []Task {
    now := time.Now().In(s.wib)
    todayStart := now.Truncate(24 * time.Hour)
    todayEnd := todayStart.Add(24 * time.Hour)

    var result []Task
    for _, t := range s.tasks {
        dl := t.Deadline.In(s.wib)
        if (dl.Equal(todayStart) || dl.After(todayStart)) &&
            dl.Before(todayEnd) {
            result = append(result, t)
        }
    }
    return result
}

func (s *Scheduler) SortByDeadline() {
    sort.Slice(s.tasks, func(i, j int) bool {
        return s.tasks[i].Deadline.Before(s.tasks[j].Deadline)
    })
}

// ── Performance Monitor ───────────────────────────────────────

type PerfMonitor struct {
    records map[string][]time.Duration
}

func NewPerfMonitor() *PerfMonitor {
    return &PerfMonitor{records: make(map[string][]time.Duration)}
}

func (pm *PerfMonitor) Measure(name string, fn func()) {
    start := time.Now()
    fn()
    d := time.Since(start)
    pm.records[name] = append(pm.records[name], d)
}

func (pm *PerfMonitor) Report() {
    fmt.Println("\n=== Performance Report ===")

    names := make([]string, 0, len(pm.records))
    for name := range pm.records { names = append(names, name) }
    sort.Strings(names)

    for _, name := range names {
        durations := pm.records[name]
        if len(durations) == 0 {
            continue
        }

        var total time.Duration
        min, max := durations[0], durations[0]
        for _, d := range durations {
            total += d
            if d < min { min = d }
            if d > max { max = d }
        }
        avg := total / time.Duration(len(durations))

        fmt.Printf("  %-25s n=%d  avg=%v  min=%v  max=%v\n",
            name, len(durations), avg.Round(time.Microsecond),
            min.Round(time.Microsecond), max.Round(time.Microsecond))
    }
}

// ── Main ──────────────────────────────────────────────────────

func main() {
    wib, _ := time.LoadLocation("Asia/Jakarta")
    now := time.Now().In(wib)

    scheduler := NewScheduler()
    perf := NewPerfMonitor()

    // Add tasks
    scheduler.AddTask("Review Pull Request",
        now,
        now.Add(2*time.Hour),
        PriorityHigh,
        30*time.Minute)

    scheduler.AddTask("Deploy to Production",
        now.Add(3*time.Hour),
        now.Add(5*time.Hour),
        PriorityCritical,
        45*time.Minute)

    scheduler.AddTask("Update Documentation",
        now.Add(time.Hour),
        now.Add(24*time.Hour),
        PriorityNormal,
        2*time.Hour)

    scheduler.AddTask("Sprint Planning Meeting",
        now.Add(-30*time.Minute), // already passed
        now.Add(-15*time.Minute), // deadline already passed
        PriorityHigh,
        time.Hour)

    scheduler.AddTask("Backup Database",
        now.Add(20*time.Hour),
        now.Add(22*time.Hour),
        PriorityNormal,
        15*time.Minute)

    // Complete task 1
    _ = scheduler.Complete(1)

    // Sort by deadline
    scheduler.SortByDeadline()

    fmt.Printf("=== Task Schedule — %s ===\n\n",
        now.Format("Monday, 02 January 2006"))

    for _, t := range scheduler.tasks {
        dl := t.Deadline.In(wib)
        fmt.Printf("  [ID:%d] %s\n", t.ID, t.Name)
        fmt.Printf("    Deadline : %s\n", dl.Format("15:04 MST"))
        fmt.Printf("    Estimate : %v\n", t.Duration)
        fmt.Printf("    Status   : %s\n", t.Status())
        fmt.Println()
    }

    // Tasks due today
    today := scheduler.TasksDueToday()
    fmt.Printf("Tasks due today: %d tasks\n", len(today))

    // Simulate performance measurements
    fmt.Println("\n=== Performance Measurement Simulation ===")

    for i := 0; i < 5; i++ {
        perf.Measure("parse dates", func() {
            for j := 0; j < 1000; j++ {
                t, _ := time.Parse("2006-01-02", "2024-07-28")
                _ = t
            }
        })
    }

    for i := 0; i < 5; i++ {
        perf.Measure("format dates", func() {
            for j := 0; j < 1000; j++ {
                s := now.Format("2006-01-02 15:04:05")
                _ = s
            }
        })
    }

    perf.Measure("parse RFC3339", func() {
        for j := 0; j < 1000; j++ {
            t, _ := time.Parse(time.RFC3339, "2024-07-28T15:30:45+07:00")
            _ = t
        }
    })

    perf.Report()

    // Demonstrate timezones
    fmt.Println("\n=== Time in Various Zones ===")
    zones := []struct {
        name     string
        location string
    }{
        {"WIB", "Asia/Jakarta"},
        {"WITA", "Asia/Makassar"},
        {"WIT", "Asia/Jayapura"},
        {"Tokyo", "Asia/Tokyo"},
        {"UTC", "UTC"},
        {"New York", "America/New_York"},
    }

    for _, z := range zones {
        loc, err := time.LoadLocation(z.location)
        if err != nil {
            continue
        }
        fmt.Printf("  %-10s %s\n", z.name+":", now.In(loc).Format("15:04:05 MST (UTC-07)"))
    }

    // Demonstrate duration parsing
    fmt.Println("\n=== Duration Operations ===")
    durations := []string{"1h30m", "45m", "2h", "90s", "1.5h"}
    for _, ds := range durations {
        d, err := time.ParseDuration(ds)
        if err != nil {
            fmt.Printf("  %s: error\n", ds)
            continue
        }
        fmt.Printf("  %-8s = %v (%.0f minutes)\n", ds, d, d.Minutes())
    }
}

Summary #

  • Go’s reference time is 2006-01-02 15:04:05 -0700 — not abstract symbols like YYYY, but actual values where each component indicates its position in the format.
  • Use time.RFC3339 (2006-01-02T15:04:05Z07:00) as the standard format for APIs and storage.
  • time.Parse always produces UTC if the string has no timezone — use time.ParseInLocation for timezone-less strings that aren’t actually UTC.
  • time.Duration is an int64 of nanoseconds — use the time.Second, time.Minute, etc. constants for readability.
  • time.Until(t) and time.Since(t) are more expressive than t.Sub(time.Now()).
  • Use .Equal() rather than == to compare time.Time== doesn’t account for timezones.
  • Always store times in UTC in the database; convert to a local timezone only when displaying to users.
  • time.NewTicker for periodic execution; time.NewTimer for a cancellable one-time delay — always call .Stop().
  • Measure performance with start := time.Now() and time.Since(start) — or use defer to measure a function’s full duration.
  • time.Truncate rounds down (start of hour, start of day); time.Round rounds to the nearest.

← Previous: Map   Next: Regex →

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