manejo-fechas-horas-timers-go

Dates, Times, and Timers in Go with the time Package

  • 5 min

time.Time is Go’s type for representing a specific moment in time. From there, we can format dates, measure durations, and schedule timers.

Time is complicated: leap years, daylight saving changes, and zones that shift due to political decisions.

The standard library time represents instants with nanosecond resolution (the actual clock precision depends on the system). Additionally, it has a unique way of defining date formats.

We’ll measure durations, convert time zones, and understand why the number 2006 appears in Go’s formats.

The Current Instant: time.Time

The main type is time.Time. It represents an instant and can include a monotonic clock reading for measuring intervals within the same process.

package main

import (
    "fmt"
    "time"
)

func main() {
    // Get the current instant
    now := time.Now()

    fmt.Println("Now:", now)
    // Output: 2023-10-27 10:00:00.000000 +0200 CEST m=+0.000

    // Get specific parts
    fmt.Println("Year:", now.Year())
    fmt.Println("Month:", now.Month())
    fmt.Println("Day:", now.Day())
}
Copied!

Formatting and Parsing: The Reference Date

In Java or Python, to format a date you use codes like YYYY-MM-DD HH:mm:ss. In Go, NO.

In Go, to define a format, you must write how a specific reference date would look in that format.

The reference date is:

Monday, January 2nd, 2006, at 15:04:05 (MST)

Why this strange date? Because when ordered in the American style, the numbers are: 1 2 3 4 5 6.

  • Month: 01
  • Day: 02
  • Hour: 03 (PM) -> 15
  • Minute: 04
  • Second: 05
  • Year: 2006

Formatting an Instant

t := time.Now()

// I want format: Year-Month-Day
fmt.Println(t.Format("2006-01-02"))

// I want format: Day/Month/Year Hour:Minute
fmt.Println(t.Format("02/01/2006 15:04"))

// I want format: Hour PM
fmt.Println(t.Format("03:04 PM"))
Copied!

Parsing Text

To convert text into a date, we use time.Parse.

layout := "2006-01-02"
text := "2023-12-31"

date, err := time.Parse(layout, text)
if err != nil {
    panic(err)
}
fmt.Println(date) // 2023-12-31 00:00:00 +0000 UTC
Copied!

By default, time.Parse assumes UTC. If you want to parse a date in the local or a specific time zone, use time.ParseInLocation.

Time Arithmetic and Durations

Go has a specific type for time intervals: time.Duration. Internally, it’s an int64 counting nanoseconds, but Go provides constants for easy operations: time.Second, time.Minute, time.Hour.

Adding and Subtracting

now := time.Now()

// What time will it be in 2 and a half hours?
future := now.Add(2*time.Hour + 30*time.Minute)

// What time was it 10 minutes ago?
past := now.Add(-10 * time.Minute)
Copied!

Calculating the Difference (Sub and Since)

To measure how long something takes:

start := time.Now()

// ... expensive operation ...
time.Sleep(2 * time.Second)

// Option 1: Subtract instants
duration := time.Now().Sub(start)

// Option 2: Use Since (More idiomatic)
duration = time.Since(start)

fmt.Println("It took:", duration) // "It took: 2.001s"
Copied!

If both values retain their monotonic clock reading, Sub and Since use it. Thus, a system clock adjustment does not alter the measurement.

Time Zones (Location)

Handling time zones is important. An AWS server might be in UTC, but your users in Spain want to see the time in CET/CEST.

To change the time zone of a time.Time object, we use the .In() method.

func main() {
    t := time.Now() // System local time

    // Load a time zone (requires the OS or Go time zone database)
    locNY, err := time.LoadLocation("America/New_York")
    if err != nil {
        fmt.Println("Could not load location:", err)
        return
    }

    // Convert the instant to NY time
    tNY := t.In(locNY)

    fmt.Println("Local:", t)
    fmt.Println("NY:   ", tNY)
}
Copied!

Timers and Tickers

The time package integrates perfectly with Go’s Channels, allowing us to manage time-based events.

time.Sleep

Suspends the current goroutine for at least the specified duration. It does not block other goroutines on its own.

time.Sleep(1 * time.Second)
Copied!

time.After: A One-Shot Channel

Returns a channel that receives a value after the specified time. Very useful in select for Timeouts.

ch := make(chan string)

select {
case msg := <-ch:
    fmt.Println(msg)
case <-time.After(3 * time.Second):
    fmt.Println("Timeout: Waiting time has expired")
}
Copied!

time.NewTicker: Recurring Events

If you need to execute something every X time (a heartbeat, cache cleanup), use a Ticker.

ticker := time.NewTicker(1 * time.Second)
// Stop it when we no longer want to receive ticks.
defer ticker.Stop()

done := make(chan struct{})

go func() {
    time.Sleep(5 * time.Second)
    close(done)
}()

for {
    select {
    case <-done:
        fmt.Println("Ticker stopped")
        return
    case t := <-ticker.C:
        fmt.Println("Tick at", t)
    }
}
Copied!

Since Go 1.23, the garbage collector can reclaim tickers that are no longer referenced, even if Stop hasn’t been called. However, Stop still expresses that we no longer want events. If the receiver is slower than the interval, the ticker may adjust or drop ticks to compensate.