Introducción: por qué el tiempo no es solo tic-tac
\n\nSi alguna vez has escrito una aplicación en Go que trabaja con fechas y usuarios de diferentes países, seguramente te has enfrentado al dolor de cabeza llamado «zonas horarias». Parece sencillo, ¿verdad? Tomas time.Now() y listo. Pero solo hasta que tu servidor está en UTC y el cliente en MSK+3, y tu «hoy» se convierte mágicamente en «ayer».
En este artículo analizaremos cómo trabajar correctamente con zonas horarias en Go, con ejemplos reales, sin rodeos y con fragmentos de código listos para usar. Aprenderás cómo no perder datos, no volverte loco durante la depuración y hacer que tu aplicación sea resistente a los husos horarios. ¡Allá vamos!
\n\nConceptos básicos: qué se esconde detrás de time.Time
\n\nEn Go, el tipo time.Time no es solo un número de milisegundos. Es una estructura que almacena dos componentes clave:
- \n
- Tiempo absoluto — la cantidad de nanosegundos desde el 1 de enero de 1970 (timestamp Unix). \n
- Location — un puntero a la zona horaria (huso horario). \n
Es precisamente Location lo que determina cómo se mostrará la hora: como 14:00 MSK o como 11:00 UTC. El error más peligroso de los principiantes es pensar que time.Now() devuelve la «hora local». En realidad, devuelve la hora actual con la zona local del servidor. Traslada el código a otro servidor y todo «se desvía».
Cómo crear tiempo con una zona específica
\n\nLa forma más sencilla es usar time.LoadLocation. Carga la zona desde la base de datos del sistema IANA (generalmente en /usr/share/zoneinfo).
package main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc main() {\n loc, err := time.LoadLocation("Europe/Moscow")\n if err != nil {\n panic(err)\n }\n\n // Creamos tiempo en una zona específica\n t := time.Date(2025, 3, 15, 14, 30, 0, 0, loc)\n fmt.Println(t) // 2025-03-15 14:30:00 +0300 MSK\n}\n\n\nTen en cuenta: time.Date recibe Location como último argumento. Si pasas time.UTC o time.Local, el comportamiento será diferente.
¿Y si la zona no está en el sistema?
\n\nA veces (por ejemplo, en imágenes Docker mínimas) la base de datos zoneinfo no está presente. La solución es usar time.FixedZone para crear una zona con un desplazamiento fijo:
loc := time.FixedZone("MSK", 3*60*60) // +3 horas desde UTC\nt := time.Now().In(loc)\nfmt.Println(t)\n\n\nInconveniente: FixedZone no conoce el horario de verano ni los cambios históricos. Para la mayoría de las aplicaciones modernas esto es aceptable, pero si necesitas precisión histórica (por ejemplo, para operaciones financieras del año 2014), usa LoadLocation.
Regla principal del desarrollo backend: almacenar en UTC
\n\nLa regla de oro para trabajar con zonas horarias en Go (y en cualquier otro lenguaje) es almacenar siempre el tiempo en UTC y convertir solo a nivel de visualización. ¿Por qué?
\n\n- \n
- El servidor puede mudarse a otro huso horario. \n
- Los usuarios pueden estar en diferentes zonas. \n
- La comparación y ordenación del tiempo UTC es trivial. \n
- Los registros (logs) son más fáciles de analizar. \n
Así es como se ve en la práctica:
\n\n// Obtenemos la hora del usuario (por ejemplo, desde un formulario)\nuserTime := "2025-03-15 14:30"\nuserLoc, _ := time.LoadLocation("Asia/Tokyo")\n\n// Parseamos como hora local del usuario\nt, _ := time.ParseInLocation("2006-01-02 15:04", userTime, userLoc)\n\n// Convertimos a UTC para almacenar\nutcTime := t.UTC()\nfmt.Println(utcTime) // 2025-03-15 05:30:00 +0000 UTC\n\n\nError típico: pérdida de Location al serializar
\n\nCuando guardas time.Time en una base de datos (por ejemplo, PostgreSQL a través de database/sql), el controlador puede «perder» el Location y dejarlo en UTC o Local. Para evitar sorpresas, siempre convierte