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 --> Num7Each 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.Parsealways assumes UTC if the string contains no timezone information. For input from users or databases without a timezone marker, always usetime.ParseInLocationwith 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.Parsealways produces UTC if the string has no timezone — usetime.ParseInLocationfor timezone-less strings that aren’t actually UTC.time.Durationis anint64of nanoseconds — use thetime.Second,time.Minute, etc. constants for readability.time.Until(t)andtime.Since(t)are more expressive thant.Sub(time.Now()).- Use
.Equal()rather than==to comparetime.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.NewTickerfor periodic execution;time.NewTimerfor a cancellable one-time delay — always call.Stop().- Measure performance with
start := time.Now()andtime.Since(start)— or usedeferto measure a function’s full duration.time.Truncaterounds down (start of hour, start of day);time.Roundrounds to the nearest.