Un objeto time.Time en Go no es solo un timestamp; es una estructura que contiene dos relojes distintos: el wall clock (reloj de pared) y el monotonic clock (reloj monotónico). El reloj de pared es el tiempo que ves en tu ordenador, el cual puede ser ajustado por el protocolo NTP o cambios manuales, provocando saltos hacia adelante o hacia atrás. El reloj monotónico, en cambio, es un contador que aumenta de forma estrictamente lineal desde el arranque del sistema, siendo inmune a los ajustes de hora del sistema.
Esta dualidad es fundamental por una razón de diseño crítica: la medición de duraciones. Si intentas medir cuánto tarda una operación restando dos valores del reloj de pared y el sistema sincroniza la hora con NTP justo en medio, podrías obtener una duración negativa o absurdamente larga. Por eso, Go integra la lectura del reloj monotónico dentro de la misma estructura time.Time. Cuando usas time.Since(t), Go utiliza internamente la lectura monotónica para garantizar precisión incluso si el reloj del sistema salta.
Debes usar time.Now() para capturar el tiempo actual cuando planeas medir intervalos, y siempre usa el método .Equal() para comparar dos instancias de time.Time si quieres saber si representan el mismo instante, ya que la comparación con == fallará si una de las variables contiene información monotónica y la otra no. Si necesitas que tu binario sea totalmente autónomo y no dependa de la base de datos de zonas horarias del sistema operativo (común en contenedores minimalistas de Docker), puedes importar time/tzdata [disponible desde Go 1.15]. Si ignoras estas diferencias, tu sistema será vulnerable a errores sutiles en la lógica de timeouts, comparaciones fallidas en tests y cálculos de latencia erróneos en entornos de producción con alta deriva de reloj.
package main
import (
"encoding/json"
"fmt"
"time"
_ "time/tzdata" // Importación para embeber la base de datos de zonas horarias
)
func main() {
// 1. La trampa de la monotonicidad y la serialización
// time.Now() incluye información del reloj monotónico.
start := time.Now()
fmt.Printf("Inicio (con monotónico): %v\n", start)
// Al serializar a JSON, la parte monotónica se pierde porque el estándar
// RFC 3339 solo representa el tiempo real (wall clock).
data, _ := json.Marshal(start)
var endSerialized time.Time
_ = json.Unmarshal(data, &endSerialized)
fmt.Printf("Serializado (sin monotónico): %v\n", endSerialized)
// ERROR: La comparación con == fallará aunque representen el mismo instante
// porque los campos internos de 'wall' (que incluyen el reloj monotónico) difieren.
fmt.Printf("¿Son iguales con ==? %v\n", start == endSerialized)
// CORRECTO: .Equal() ignora la parte monotónica y solo mira el instante real.
fmt.Printf("¿Son iguales con .Equal()? %v\n", start.Equal(endSerialized))
// 2. El peligro de time.Parse vs time.ParseInLocation
layout := "2006-01-02 15:04:05"
str := "2023-10-27 10:00:00"
// time.Parse asume UTC si no se especifica la zona, lo que suele ser un error
// si la intención es representar una hora local.
tParse, _ := time.Parse(layout, str)
// time.ParseInLocation permite controlar explícitamente la zona horaria.
loc, _ := time.LoadLocation("America/Argentina/Buenos_Aires")
tLoc, _ := time.ParseInLocation(layout, str, loc)
fmt.Printf("\nParse (UTC): %v\n", tParse)
fmt.Printf("ParseInLocation (Local): %v\n", tLoc)
// 3. Medir duraciones: Unix vs time.Since
// Evita usar t.Unix() para medir lapsos.
t1 := time.Now()
// Simulamos un retraso
time.Sleep(10 * time.Millisecond)
t2 := time.Now()
// Correcto: time.Since usa el reloj monotónico de forma transparente.
durationCorrecta := time.Since(t1)
// Incorrecto: t.Unix() extrae el reloj de pared.
// Si el reloj del sistema retrocede 1 segundo por NTP, esta resta será errónea.
durationInexacta := t2.Unix() - t1.Unix()
fmt.Printf("\nDuración real (Since): %v\n", durationCorrecta)
fmt.Printf("Duración por Unix (segundos): %d\n", durationInexacta)
}
Análisis del ejemplo
En el primer bloque, observamos cómo start contiene información de un reloj que no se puede ver en un string. Al ejecutar json.Marshal, solo se guarda el tiempo de pared en formato texto. Cuando hacemos json.Unmarshal en endSerialized, el objeto resultante es “puro” (solo wall clock). Esto hace que start == endSerialized sea false, una de las causas más comunes de fallos en tests que comparan tiempos capturados de un API y un objeto deserializado. La solución es usar siempre t1.Equal(t2).
En la sección de zonas horarias, el ejemplo demuestra que time.Parse es una función “peligrosa” si esperas que el tiempo se interprete en tu zona local; siempre es preferible time.ParseInLocation para evitar errores de offset de UTC.
Finalmente, la comparación de duraciones subraya por qué time.Since(t) es la forma estándar en Go. Al usar t.Unix(), estás operando sobre un entero que representa segundos del reloj de pared, perdiendo toda la precisión de nanosegundos y la seguridad del reloj monotónico.
El error frecuente
Un error clásico es intentar calcular la duración de un proceso usando segundos Unix para evitar la complejidad de los tipos time.Duration:
// MAL: Vulnerable a ajustes de NTP start := time.Now().Unix() // ... proceso que tarda 2 segundos ... elapsed := time.Now().Unix() - start
Si durante la ejecución un proceso de sincronización NTP ajusta el reloj del sistema hacia atrás 5 segundos, elapsed resultará en -3, lo que puede causar panics en lógicas de timeout o errores de negocio en sistemas de facturación o logs.
N° 237