Introduction: Why Time is More Than Just Tick-Tock
\n\nIf you've ever written a Go application that works with dates and users from different countries, you've likely encountered the headache called "time zones." It seems simple enough, right? Just use time.Now() and you're done. That is, until your server is in UTC and your client is in MSK+3, and your "today" magically turns into "yesterday."
In this article, we'll break down how to work with time zones in Go properly, using real-world examples, no fluff, and ready-to-use code snippets. You'll learn how not to lose data, how to keep your sanity while debugging, and how to make your application timezone-resilient. Let's go!
\n\nThe Basics: What Lies Beneath time.Time
\n\nIn Go, the time.Time type is not just a number of milliseconds. It's a structure that stores two key components:
- \n
- Absolute time — the number of nanoseconds since January 1, 1970 (Unix timestamp). \n
- Location — a pointer to the time zone. \n
It is the Location that determines how the time is displayed: as 14:00 MSK or as 11:00 UTC. The most dangerous misconception for beginners is thinking that time.Now() returns "local time." In reality, it returns the current time with the server's local zone. Move the code to another server, and everything "drifts."
How to Create Time with a Specific Zone
\n\nThe simplest way is to use time.LoadLocation. It loads the zone from the system's IANA database (usually located in /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 // Create time in a specific zone\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\nNote: time.Date takes the Location as the last argument. If you pass time.UTC or time.Local, the behavior will be different.
What if the Zone is Not in the System?
\n\nSometimes (for example, in minimal Docker images), the zoneinfo database is missing. The solution is to use time.FixedZone to create a zone with a fixed offset:
loc := time.FixedZone("MSK", 3*60*60) // +3 hours from UTC\nt := time.Now().In(loc)\nfmt.Println(t)\n\n\nThe downside: FixedZone doesn't know about daylight saving time or historical changes. For most modern applications, this is acceptable, but if you need historical accuracy (e.g., for financial transactions from 2014), use LoadLocation.
The Golden Rule of Server Development: Store in UTC
\n\nThe golden rule for working with time zones in Go (and any other language) is to always store time in UTC and convert only at the display level. Why?
\n\n- \n
- The server might move to a different time zone. \n
- Users can be from different zones. \n
- Comparing and sorting UTC time is trivial. \n
- Logs are easier to analyze. \n
Here's how it looks in practice:
\n\n// Get the user's time (e.g., from a form)\nuserTime := "2025-03-15 14:30"\nuserLoc, _ := time.LoadLocation("Asia/Tokyo")\n\n// Parse as the user's local time\nt, _ := time.ParseInLocation("2006-01-02 15:04", userTime, userLoc)\n\n// Convert to UTC for storage\nutcTime := t.UTC()\nfmt.Println(utcTime) // 2025-03-15 05:30:00 +0000 UTC\n\n\nCommon Mistake: Location Reset During Serialization
\n\nWhen you save time.Time to a database (e.g., PostgreSQL via database/sql), the driver might "reset" the Location to UTC or Local. To avoid surprises, always convert