Go time Package: Comprehensive Tutorial
There's something refreshingly straightforward about Go's approach to time. No inheritance hierarchies, no twelve different classes to choose from—just the time
package and its beautifully designed types. It's type-safe, it's efficient, and once you get past that quirky date format syntax, it's honestly a joy to work with. Let's dive in.
The time.Time Type
Creating Time Values
package main
import (
"fmt"
"time"
)
func main() {
// Current time (local timezone)
now := time.Now()
fmt.Println(now) // 2024-01-15 19:00:00.123456789 -0500 EST
// Current time in UTC
utcNow := time.Now().UTC()
fmt.Println(utcNow) // 2024-01-15 19:00:00.123456789 +0000 UTC
// From components
date := time.Date(2024, 1, 15, 19, 0, 0, 0, time.UTC)
// Args: year, month, day, hour, min, sec, nsec, location
// From Unix timestamp (seconds)
timestamp := int64(1705341600)
fromUnix := time.Unix(timestamp, 0)
fmt.Println(fromUnix) // 2024-01-15 19:00:00 +0000 UTC
// From Unix timestamp with nanoseconds
fromUnixNano := time.Unix(timestamp, 123456789)
// Parse from string
parsed, err := time.Parse(time.RFC3339, "2024-01-15T19:00:00Z")
if err != nil {
panic(err)
}
fmt.Println(parsed)
// Parse with custom layout
layout := "2006-01-02 15:04:05"
custom, err := time.Parse(layout, "2024-01-15 19:00:00")
if err != nil {
panic(err)
}
fmt.Println(custom)
}
Converting to Timestamps
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2024, 1, 15, 19, 0, 0, 0, time.UTC)
// To Unix timestamp (seconds)
unixSec := t.Unix()
fmt.Println(unixSec) // 1705341600
// To Unix timestamp (milliseconds)
unixMilli := t.UnixMilli()
fmt.Println(unixMilli) // 1705341600000
// To Unix timestamp (microseconds)
unixMicro := t.UnixMicro()
fmt.Println(unixMicro) // 1705341600000000
// To Unix timestamp (nanoseconds)
unixNano := t.UnixNano()
fmt.Println(unixNano) // 1705341600000000000
// To RFC3339 string (ISO 8601)
rfc3339 := t.Format(time.RFC3339)
fmt.Println(rfc3339) // 2024-01-15T19:00:00Z
// To RFC3339Nano (with nanoseconds)
rfc3339Nano := t.Format(time.RFC3339Nano)
fmt.Println(rfc3339Nano)
// Custom formatting
formatted := t.Format("2006-01-02 15:04:05")
fmt.Println(formatted) // 2024-01-15 19:00:00
}
Go's "Magic" Date Format (Yeah, It's Weird at First)
Here's where Go gets... unique. Instead of %Y-%m-%d
or yyyy-MM-dd
, Go uses an actual reference date. Why? Because the Go team decided memorizing format codes was silly. Is it weird? Yes. Will you love it once you get used to it? Also yes.
The Reference Date: Mon Jan 2 15:04:05 MST 2006 (Remember This!)
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2024, 1, 15, 19, 0, 0, 0, time.UTC)
/*
Reference date components:
Mon Jan 2 15:04:05 MST 2006
Year: 2006 (or 06)
Month: 01 (or 1, Jan, January)
Day: 02 (or 2, _2)
Hour: 15 (or 03, 3)
Minute: 04 (or 4)
Second: 05 (or 5)
Weekday: Mon (or Monday)
Timezone: MST (or -0700, -07:00, Z07:00)
*/
// Date formats
fmt.Println(t.Format("2006")) // 2024 - Year (4 digit)
fmt.Println(t.Format("06")) // 24 - Year (2 digit)
fmt.Println(t.Format("01")) // 01 - Month (2 digit)
fmt.Println(t.Format("1")) // 1 - Month (1 digit)
fmt.Println(t.Format("Jan")) // Jan - Month short
fmt.Println(t.Format("January")) // January - Month full
fmt.Println(t.Format("02")) // 15 - Day (2 digit)
fmt.Println(t.Format("2")) // 15 - Day (1 digit)
fmt.Println(t.Format("_2")) // 15 - Day (space padded)
// Time formats
fmt.Println(t.Format("15")) // 19 - Hour 24h
fmt.Println(t.Format("03")) // 07 - Hour 12h
fmt.Println(t.Format("3")) // 7 - Hour 12h (no leading 0)
fmt.Println(t.Format("04")) // 00 - Minute
fmt.Println(t.Format("05")) // 00 - Second
fmt.Println(t.Format("PM")) // PM - AM/PM
fmt.Println(t.Format("pm")) // pm - am/pm
// Weekday
fmt.Println(t.Format("Mon")) // Mon - Weekday short
fmt.Println(t.Format("Monday")) // Monday - Weekday full
// Timezone
fmt.Println(t.Format("MST")) // UTC - Timezone name
fmt.Println(t.Format("-0700")) // +0000 - Offset
fmt.Println(t.Format("-07:00")) // +00:00 - Offset with colon
fmt.Println(t.Format("Z0700")) // Z - UTC as Z, else offset
fmt.Println(t.Format("Z07:00")) // Z - UTC as Z, else offset with colon
// Common patterns
fmt.Println(t.Format("2006-01-02")) // 2024-01-15
fmt.Println(t.Format("2006-01-02 15:04:05")) // 2024-01-15 19:00:00
fmt.Println(t.Format("January 2, 2006")) // January 15, 2024
fmt.Println(t.Format("Monday, January 2, 2006")) // Monday, January 15, 2024
fmt.Println(t.Format("Mon Jan _2 15:04:05 MST 2006")) // Mon Jan 15 19:00:00 UTC 2024
}
Predefined Layouts
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
// Predefined constants
fmt.Println(t.Format(time.Layout)) // 01/02 03:04:05PM '06 -0700
fmt.Println(t.Format(time.ANSIC)) // Mon Jan _2 15:04:05 2006
fmt.Println(t.Format(time.UnixDate)) // Mon Jan _2 15:04:05 MST 2006
fmt.Println(t.Format(time.RubyDate)) // Mon Jan 02 15:04:05 -0700 2006
fmt.Println(t.Format(time.RFC822)) // 02 Jan 06 15:04 MST
fmt.Println(t.Format(time.RFC822Z)) // 02 Jan 06 15:04 -0700
fmt.Println(t.Format(time.RFC850)) // Monday, 02-Jan-06 15:04:05 MST
fmt.Println(t.Format(time.RFC1123)) // Mon, 02 Jan 2006 15:04:05 MST
fmt.Println(t.Format(time.RFC1123Z)) // Mon, 02 Jan 2006 15:04:05 -0700
fmt.Println(t.Format(time.RFC3339)) // 2006-01-02T15:04:05Z07:00
fmt.Println(t.Format(time.RFC3339Nano)) // 2006-01-02T15:04:05.999999999Z07:00
fmt.Println(t.Format(time.Kitchen)) // 3:04PM
fmt.Println(t.Format(time.Stamp)) // Jan _2 15:04:05
fmt.Println(t.Format(time.StampMilli)) // Jan _2 15:04:05.000
fmt.Println(t.Format(time.StampMicro)) // Jan _2 15:04:05.000000
fmt.Println(t.Format(time.StampNano)) // Jan _2 15:04:05.000000000
}
Parsing Time Strings
Basic Parsing
package main
import (
"fmt"
"time"
)
func main() {
// Parse with layout
layout := "2006-01-02 15:04:05"
str := "2024-01-15 19:00:00"
parsed, err := time.Parse(layout, str)
if err != nil {
panic(err)
}
fmt.Println(parsed)
// Parse in location
loc, _ := time.LoadLocation("America/New_York")
inLocation, err := time.ParseInLocation(layout, str, loc)
if err != nil {
panic(err)
}
fmt.Println(inLocation)
// Parse RFC3339 (ISO 8601)
iso := "2024-01-15T19:00:00Z"
isoTime, err := time.Parse(time.RFC3339, iso)
if err != nil {
panic(err)
}
fmt.Println(isoTime)
// Parse with timezone
withTz := "2024-01-15T19:00:00-05:00"
tzTime, err := time.Parse(time.RFC3339, withTz)
if err != nil {
panic(err)
}
fmt.Println(tzTime)
}
Safe Parsing Helper
package main
import (
"fmt"
"time"
)
// ParseTime safely parses various datetime formats
func ParseTime(input string) (time.Time, error) {
// Try common formats in order
formats := []string{
time.RFC3339, // 2006-01-02T15:04:05Z07:00
time.RFC3339Nano, // With nanoseconds
"2006-01-02 15:04:05", // SQL datetime
"2006-01-02", // Date only
time.RFC1123Z, // RFC 1123 with timezone
time.RFC822Z, // RFC 822 with timezone
}
for _, format := range formats {
if t, err := time.Parse(format, input); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("unable to parse time: %s", input)
}
func main() {
examples := []string{
"2024-01-15T19:00:00Z",
"2024-01-15 19:00:00",
"2024-01-15",
"Mon, 15 Jan 2024 19:00:00 +0000",
}
for _, example := range examples {
t, err := ParseTime(example)
if err != nil {
fmt.Printf("Error parsing '%s': %v\n", example, err)
} else {
fmt.Printf("Parsed '%s' → %v\n", example, t)
}
}
}
Time Arithmetic
Go's Duration
type is where things get really nice. It's just an int64 of nanoseconds under the hood, but the API makes it feel natural.
Duration Type (Nanoseconds All The Way Down)
package main
import (
"fmt"
"time"
)
func main() {
// Create durations
second := time.Second
minute := time.Minute
hour := time.Hour
// Duration constants
fmt.Println(time.Nanosecond) // 1ns
fmt.Println(time.Microsecond) // 1µs = 1000ns
fmt.Println(time.Millisecond) // 1ms = 1000µs
fmt.Println(time.Second) // 1s = 1000ms
fmt.Println(time.Minute) // 1m = 60s
fmt.Println(time.Hour) // 1h = 60m
// Create custom durations
twoHours := 2 * time.Hour
thirtyMinutes := 30 * time.Minute
fiveSeconds := 5 * time.Second
// Complex duration
complex := 2*time.Hour + 30*time.Minute + 15*time.Second
fmt.Println(complex) // 2h30m15s
// Duration arithmetic
sum := time.Hour + 30*time.Minute // 1h30m
diff := 2*time.Hour - 30*time.Minute // 1h30m
doubled := 2 * time.Hour // 2h
half := time.Hour / 2 // 30m
// Convert duration to units
d := 90 * time.Minute
fmt.Println(d.Hours()) // 1.5
fmt.Println(d.Minutes()) // 90
fmt.Println(d.Seconds()) // 5400
fmt.Println(d.Milliseconds()) // 5400000
fmt.Println(d.Microseconds()) // 5400000000
fmt.Println(d.Nanoseconds()) // 5400000000000
// Truncate and round
d2 := 125 * time.Minute
fmt.Println(d2.Truncate(time.Hour)) // 2h
fmt.Println(d2.Round(time.Hour)) // 2h
}
Add and Sub
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2024, 1, 15, 19, 0, 0, 0, time.UTC)
// Add duration
oneHourLater := t.Add(time.Hour)
oneDayLater := t.Add(24 * time.Hour)
twoWeeksLater := t.Add(14 * 24 * time.Hour)
fmt.Println(oneHourLater) // 2024-01-15 20:00:00 +0000 UTC
fmt.Println(oneDayLater) // 2024-01-16 19:00:00 +0000 UTC
fmt.Println(twoWeeksLater) // 2024-01-29 19:00:00 +0000 UTC
// Subtract duration (add negative)
oneHourEarlier := t.Add(-time.Hour)
fmt.Println(oneHourEarlier) // 2024-01-15 18:00:00 +0000 UTC
// Difference between times
start := time.Now()
time.Sleep(2 * time.Second)
end := time.Now()
elapsed := end.Sub(start)
fmt.Printf("Elapsed: %v\n", elapsed) // Elapsed: 2.001234s
fmt.Printf("Seconds: %.2f\n", elapsed.Seconds())
// Until and Since
future := time.Now().Add(5 * time.Minute)
past := time.Now().Add(-5 * time.Minute)
untilFuture := time.Until(future) // Duration until future
sincePast := time.Since(past) // Duration since past
fmt.Printf("Until future: %v\n", untilFuture)
fmt.Printf("Since past: %v\n", sincePast)
}
AddDate for Calendar Arithmetic
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2024, 1, 15, 19, 0, 0, 0, time.UTC)
// AddDate(years, months, days)
nextYear := t.AddDate(1, 0, 0)
nextMonth := t.AddDate(0, 1, 0)
nextWeek := t.AddDate(0, 0, 7)
tomorrow := t.AddDate(0, 0, 1)
fmt.Println(nextYear) // 2025-01-15 19:00:00 +0000 UTC
fmt.Println(nextMonth) // 2024-02-15 19:00:00 +0000 UTC
fmt.Println(nextWeek) // 2024-01-22 19:00:00 +0000 UTC
fmt.Println(tomorrow) // 2024-01-16 19:00:00 +0000 UTC
// Subtract (use negative values)
lastYear := t.AddDate(-1, 0, 0)
lastMonth := t.AddDate(0, -1, 0)
yesterday := t.AddDate(0, 0, -1)
// Complex calendar arithmetic
complex := t.AddDate(1, 2, 15) // 1 year, 2 months, 15 days
fmt.Println(complex) // 2025-03-30 19:00:00 +0000 UTC
// Handles month boundaries correctly
jan31 := time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC)
feb := jan31.AddDate(0, 1, 0)
fmt.Println(feb) // 2024-03-02 (Feb has 29 days in 2024)
// Last day of month calculation
firstOfNextMonth := time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC)
lastOfThisMonth := firstOfNextMonth.AddDate(0, 0, -1)
fmt.Println(lastOfThisMonth) // 2024-01-31
}
Timezone Handling
time.Location Type
package main
import (
"fmt"
"time"
)
func main() {
// Load timezone
nyc, err := time.LoadLocation("America/New_York")
if err != nil {
panic(err)
}
tokyo, _ := time.LoadLocation("Asia/Tokyo")
london, _ := time.LoadLocation("Europe/London")
// UTC and Local are predefined
utc := time.UTC
local := time.Local
// Create time in specific timezone
nycTime := time.Date(2024, 1, 15, 14, 0, 0, 0, nyc)
fmt.Println(nycTime) // 2024-01-15 14:00:00 -0500 EST
// Current time in timezone
nycNow := time.Now().In(nyc)
tokyoNow := time.Now().In(tokyo)
fmt.Printf("NYC: %s\n", nycNow.Format("15:04:05 MST"))
fmt.Printf("Tokyo: %s\n", tokyoNow.Format("15:04:05 MST"))
// Get location from time
loc := nycTime.Location()
fmt.Println(loc.String()) // America/New_York
}
Converting Between Timezones
package main
import (
"fmt"
"time"
)
func main() {
// Create time in UTC
utcTime := time.Date(2024, 1, 15, 19, 0, 0, 0, time.UTC)
// Load timezones
nyc, _ := time.LoadLocation("America/New_York")
tokyo, _ := time.LoadLocation("Asia/Tokyo")
london, _ := time.LoadLocation("Europe/London")
// Convert to different timezones
nycTime := utcTime.In(nyc)
tokyoTime := utcTime.In(tokyo)
londonTime := utcTime.In(london)
// Same moment in time, different representation
fmt.Println("UTC: ", utcTime.Format("2006-01-02 15:04:05 MST"))
fmt.Println("NYC: ", nycTime.Format("2006-01-02 15:04:05 MST"))
fmt.Println("Tokyo: ", tokyoTime.Format("2006-01-02 15:04:05 MST"))
fmt.Println("London: ", londonTime.Format("2006-01-02 15:04:05 MST"))
// Output:
// UTC: 2024-01-15 19:00:00 UTC
// NYC: 2024-01-15 14:00:00 EST
// Tokyo: 2024-01-16 04:00:00 JST
// London: 2024-01-15 19:00:00 GMT
// Verify same moment
fmt.Println("Same moment?", utcTime.Equal(nycTime)) // true
fmt.Println("Unix:", utcTime.Unix()) // Same timestamp
fmt.Println("Unix:", nycTime.Unix()) // Same timestamp
}
Handling DST Transitions
package main
import (
"fmt"
"time"
)
func main() {
nyc, _ := time.LoadLocation("America/New_York")
// Spring forward: March 10, 2024, 2:00 AM → 3:00 AM
// Attempting to create 2:30 AM (doesn't exist)
nonExistent := time.Date(2024, 3, 10, 2, 30, 0, 0, nyc)
fmt.Println(nonExistent.Format("2006-01-02 15:04:05 MST"))
// 2024-03-10 03:30:00 EDT (automatically adjusted to 3:30 AM)
// Fall back: November 3, 2024, 2:00 AM → 1:00 AM
// 1:30 AM occurs twice
ambiguous := time.Date(2024, 11, 3, 1, 30, 0, 0, nyc)
fmt.Println(ambiguous.Format("2006-01-02 15:04:05 MST"))
// 2024-11-03 01:30:00 EDT (first occurrence)
// Check DST status
summer := time.Date(2024, 7, 15, 12, 0, 0, 0, nyc)
winter := time.Date(2024, 1, 15, 12, 0, 0, 0, nyc)
_, summerOffset := summer.Zone()
_, winterOffset := winter.Zone()
fmt.Printf("Summer offset: %d hours\n", summerOffset/3600) // -4 (EDT)
fmt.Printf("Winter offset: %d hours\n", winterOffset/3600) // -5 (EST)
// Get timezone name and offset
zone, offset := summer.Zone()
fmt.Printf("Zone: %s, Offset: %d seconds\n", zone, offset)
}
Time Comparison
Compare Methods
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
past := now.Add(-time.Hour)
future := now.Add(time.Hour)
// Equal (checks if same moment)
fmt.Println(now.Equal(now)) // true
fmt.Println(now.Equal(past)) // false
// Before
fmt.Println(past.Before(now)) // true
fmt.Println(now.Before(past)) // false
// After
fmt.Println(future.After(now)) // true
fmt.Println(now.After(future)) // false
// Compare (returns -1, 0, or 1)
fmt.Println(past.Compare(now)) // -1 (past < now)
fmt.Println(now.Compare(now)) // 0 (equal)
fmt.Println(future.Compare(now)) // 1 (future > now)
// IsZero (check for zero value)
var zero time.Time
fmt.Println(zero.IsZero()) // true
fmt.Println(now.IsZero()) // false
// Equal handles timezone differences
utc := time.Date(2024, 1, 15, 19, 0, 0, 0, time.UTC)
nyc, _ := time.LoadLocation("America/New_York")
est := time.Date(2024, 1, 15, 14, 0, 0, 0, nyc)
fmt.Println(utc.Equal(est)) // true (same moment, different zones)
}
Working with Time Components
Extracting Components
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2024, 1, 15, 19, 30, 45, 123456789, time.UTC)
// Date components
year := t.Year() // 2024
month := t.Month() // January (time.Month type)
day := t.Day() // 15
yearDay := t.YearDay() // 15 (day of year)
weekday := t.Weekday() // Monday (time.Weekday type)
// Time components
hour := t.Hour() // 19
minute := t.Minute() // 30
second := t.Second() // 45
nanosec := t.Nanosecond() // 123456789
fmt.Printf("Date: %d-%s-%d\n", year, month, day)
fmt.Printf("Time: %02d:%02d:%02d.%09d\n", hour, minute, second, nanosec)
fmt.Printf("Weekday: %s\n", weekday)
fmt.Printf("Day of year: %d\n", yearDay)
// ISO week
isoYear, isoWeek := t.ISOWeek()
fmt.Printf("ISO week: %d-W%02d\n", isoYear, isoWeek)
// Date() returns year, month, day
y, m, d := t.Date()
fmt.Printf("Date: %d-%s-%d\n", y, m, d)
// Clock() returns hour, min, sec
h, min, s := t.Clock()
fmt.Printf("Clock: %02d:%02d:%02d\n", h, min, s)
// Month and Weekday are typed enums
fmt.Println(month == time.January) // true
fmt.Println(weekday == time.Monday) // true
}
Truncate and Round
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2024, 1, 15, 19, 34, 56, 123456789, time.UTC)
// Truncate (round down)
fmt.Println(t.Truncate(time.Hour)) // 2024-01-15 19:00:00
fmt.Println(t.Truncate(time.Minute)) // 2024-01-15 19:34:00
fmt.Println(t.Truncate(time.Second)) // 2024-01-15 19:34:56
// Round (nearest)
fmt.Println(t.Round(time.Hour)) // 2024-01-15 20:00:00
fmt.Println(t.Round(time.Minute)) // 2024-01-15 19:35:00
fmt.Println(t.Round(time.Second)) // 2024-01-15 19:34:56
// Round to specific durations
fmt.Println(t.Round(15 * time.Minute)) // Round to nearest 15 min
fmt.Println(t.Round(5 * time.Minute)) // Round to nearest 5 min
// Truncate to day
dayStart := t.Truncate(24 * time.Hour)
fmt.Println(dayStart) // 2024-01-15 00:00:00
}
Database Integration
Storing Timestamps
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/lib/pq" // PostgreSQL driver
)
type Event struct {
ID int
Title string
ScheduledAt time.Time
Timezone string
CreatedAt time.Time
}
func saveEvent(db *sql.DB, event Event) error {
query := `
INSERT INTO events (title, scheduled_at, timezone, created_at)
VALUES ($1, $2, $3, $4)
RETURNING id
`
// Store as UTC
utcScheduled := event.ScheduledAt.UTC()
utcCreated := time.Now().UTC()
err := db.QueryRow(
query,
event.Title,
utcScheduled,
event.Timezone,
utcCreated,
).Scan(&event.ID)
return err
}
func findEvent(db *sql.DB, id int, timezone string) (*Event, error) {
query := `
SELECT id, title, scheduled_at, timezone, created_at
FROM events
WHERE id = $1
`
var event Event
err := db.QueryRow(query, id).Scan(
&event.ID,
&event.Title,
&event.ScheduledAt,
&event.Timezone,
&event.CreatedAt,
)
if err != nil {
return nil, err
}
// Convert to requested timezone
loc, err := time.LoadLocation(timezone)
if err != nil {
return nil, err
}
event.ScheduledAt = event.ScheduledAt.In(loc)
event.CreatedAt = event.CreatedAt.In(loc)
return &event, nil
}
func findEventsInRange(db *sql.DB, start, end time.Time) ([]Event, error) {
query := `
SELECT id, title, scheduled_at, timezone
FROM events
WHERE scheduled_at BETWEEN $1 AND $2
ORDER BY scheduled_at ASC
`
rows, err := db.Query(query, start.UTC(), end.UTC())
if err != nil {
return nil, err
}
defer rows.Close()
var events []Event
for rows.Next() {
var event Event
err := rows.Scan(
&event.ID,
&event.Title,
&event.ScheduledAt,
&event.Timezone,
)
if err != nil {
return nil, err
}
events = append(events, event)
}
return events, rows.Err()
}
func main() {
db, err := sql.Open("postgres", "postgres://user:pass@localhost/mydb?sslmode=disable")
if err != nil {
panic(err)
}
defer db.Close()
// Create event
nyc, _ := time.LoadLocation("America/New_York")
event := Event{
Title: "Team Meeting",
ScheduledAt: time.Date(2024, 1, 15, 14, 0, 0, 0, nyc),
Timezone: "America/New_York",
}
if err := saveEvent(db, event); err != nil {
panic(err)
}
// Retrieve in user's timezone
retrieved, err := findEvent(db, event.ID, "America/Los_Angeles")
if err != nil {
panic(err)
}
fmt.Printf("Event: %s at %s\n",
retrieved.Title,
retrieved.ScheduledAt.Format("2006-01-02 15:04:05 MST"),
)
}
Testing Time-Dependent Code
Want to know a secret? The best Go developers don't call time.Now()
directly in their business logic. They inject a clock. It sounds fancy, but it's actually super simple and makes testing a breeze.
Clock Interface Pattern (Your Testing Superpower)
package main
import (
"testing"
"time"
)
// Clock interface for testability
type Clock interface {
Now() time.Time
}
// RealClock uses actual system time
type RealClock struct{}
func (RealClock) Now() time.Time {
return time.Now()
}
// MockClock allows setting fixed time
type MockClock struct {
current time.Time
}
func (m *MockClock) Now() time.Time {
return m.current
}
func (m *MockClock) Set(t time.Time) {
m.current = t
}
func (m *MockClock) Advance(d time.Duration) {
m.current = m.current.Add(d)
}
// Service with injectable clock
type SessionService struct {
clock Clock
}
func NewSessionService(clock Clock) *SessionService {
return &SessionService{clock: clock}
}
func (s *SessionService) IsExpired(expiresAt time.Time) bool {
return s.clock.Now().After(expiresAt)
}
// Tests
func TestSessionExpiry(t *testing.T) {
// Create mock clock at specific time
fixedTime := time.Date(2024, 1, 15, 19, 0, 0, 0, time.UTC)
mockClock := &MockClock{current: fixedTime}
service := NewSessionService(mockClock)
// Session expires in 1 hour
expiresAt := fixedTime.Add(time.Hour)
// Should not be expired yet
if service.IsExpired(expiresAt) {
t.Error("Expected session to be valid")
}
// Advance 59 minutes (still valid)
mockClock.Advance(59 * time.Minute)
if service.IsExpired(expiresAt) {
t.Error("Expected session to still be valid")
}
// Advance 2 more minutes (now expired)
mockClock.Advance(2 * time.Minute)
if !service.IsExpired(expiresAt) {
t.Error("Expected session to be expired")
}
}
func TestDSTTransition(t *testing.T) {
nyc, _ := time.LoadLocation("America/New_York")
mockClock := &MockClock{}
// Before DST (EST)
beforeDST := time.Date(2024, 3, 9, 12, 0, 0, 0, nyc)
mockClock.Set(beforeDST)
_, offset := mockClock.Now().Zone()
if offset != -5*3600 {
t.Errorf("Expected EST offset -5h, got %d", offset/3600)
}
// After DST (EDT)
afterDST := time.Date(2024, 3, 11, 12, 0, 0, 0, nyc)
mockClock.Set(afterDST)
_, offset = mockClock.Now().Zone()
if offset != -4*3600 {
t.Errorf("Expected EDT offset -4h, got %d", offset/3600)
}
}
// Production usage with real clock
func main() {
service := NewSessionService(RealClock{})
expiresAt := time.Now().Add(time.Hour)
println(service.IsExpired(expiresAt))
}
Best Practices
1. Always Store UTC
// ✅ CORRECT: Store as UTC
func SaveEvent(db *sql.DB, title string, scheduledAt time.Time, tz string) error {
// Convert to UTC for storage
utc := scheduledAt.UTC()
_, err := db.Exec(
"INSERT INTO events (title, scheduled_at, timezone) VALUES (?, ?, ?)",
title, utc, tz,
)
return err
}
// Display in user's timezone
func DisplayEvent(db *sql.DB, id int, userTz string) (string, error) {
var scheduledAt time.Time
var title string
err := db.QueryRow("SELECT title, scheduled_at FROM events WHERE id = ?", id).
Scan(&title, &scheduledAt)
if err != nil {
return "", err
}
loc, _ := time.LoadLocation(userTz)
local := scheduledAt.In(loc)
return fmt.Sprintf("%s at %s", title, local.Format("2006-01-02 15:04:05 MST")), nil
}
2. Use time.Time, Not Pointers
// ❌ BAD: Pointer to time
type Event struct {
ScheduledAt *time.Time
}
// ✅ GOOD: Value type
type Event struct {
ScheduledAt time.Time
}
// time.Time is safe to pass by value
// Zero value (time.Time{}) is useful
// Check with IsZero() if needed
3. Handle Timezones Explicitly
// ❌ BAD: Implicit local timezone
t := time.Now() // Uses local timezone
// ✅ GOOD: Explicit timezone
utc := time.Now().UTC()
nyc, _ := time.LoadLocation("America/New_York")
nycTime := time.Now().In(nyc)
4. Use Clock Interface for Testing
// Make time testable
type Service struct {
clock Clock
}
// Production uses real time
service := &Service{clock: RealClock{}}
// Tests use mock time
mockClock := &MockClock{current: fixedTime}
service := &Service{clock: mockClock}
Common Pitfalls
1. Comparing Times from Different Zones
// ❌ WRONG: Using == (checks representation, not moment)
utc := time.Date(2024, 1, 15, 19, 0, 0, 0, time.UTC)
nyc, _ := time.LoadLocation("America/New_York")
est := time.Date(2024, 1, 15, 14, 0, 0, 0, nyc)
fmt.Println(utc == est) // false (different zones)
// ✅ CORRECT: Using Equal() (checks moment)
fmt.Println(utc.Equal(est)) // true (same moment)
2. Forgetting Nanosecond Precision
// time.Time has nanosecond precision
t1 := time.Unix(1705341600, 123456789)
t2 := time.Unix(1705341600, 987654321)
fmt.Println(t1.Unix()) // Same: 1705341600
fmt.Println(t1.Equal(t2)) // false (different nanoseconds)
3. Using Local Time in APIs
// ❌ BAD: Ambiguous
type Response struct {
Timestamp time.Time `json:"timestamp"`
}
// ✅ GOOD: Always UTC, format as ISO 8601
type Response struct {
Timestamp string `json:"timestamp"`
}
func NewResponse(t time.Time) Response {
return Response{
Timestamp: t.UTC().Format(time.RFC3339),
}
}
Performance Tips
Avoid Repeated LoadLocation
// ❌ SLOW: Load timezone every iteration
for i := 0; i < 1000; i++ {
loc, _ := time.LoadLocation("America/New_York")
t := time.Now().In(loc)
}
// ✅ FAST: Load once
loc, _ := time.LoadLocation("America/New_York")
for i := 0; i < 1000; i++ {
t := time.Now().In(loc)
}
Use time.After for Timeouts
select {
case result := <-ch:
// Process result
case <-time.After(5 * time.Second):
// Timeout
}
Wrapping Up: Go Time Patterns That Work
Look, I'll level with you. When I first saw Go's date format (remember: Mon Jan 2 15:04:05 MST 2006), I thought it was ridiculous. But you know what? After using it for a while, I actually prefer it. It's memorable, it's unambiguous, and you never have to look up whether it's %m
or %M
for minutes.
Your Go Time Essentials:
time.Time
is your timestamp (value type, not pointer—this matters!)time.Duration
is how you measure, well, durationstime.Location
keeps your timezones straight
The Patterns That Actually Work:
- UTC for storage—no compromises here
- Pass
time.Time
by value, not pointer (it's designed for this) - Use
time.Location
explicitly—implicit is where bugs hide Equal()
for comparisons (seriously, don't use==
)- Clock interface pattern for testing (game-changer)
- Validate inputs early and often
- Cache those
LoadLocation()
calls (they're not cheap)
The beautiful thing about Go's time package? It's simple without being simplistic. Once you internalize the reference date and understand the value types, everything else just clicks. You'll be writing correct, timezone-aware code without thinking about it. And isn't that exactly what we want from our tools?
Further Reading
- Complete Guide to Unix Timestamps - Foundation of time representation
- ISO 8601 Standard Explained - Date format standard
- Timezone Conversion Best Practices - Cross-language timezone patterns
- Testing Time-Dependent Code - Go testing strategies
- Database Timestamp Storage - Store Go timestamps in databases
Building Go applications with datetime? Contact us for consultation.