Wandering Thoughts archives

2020-06-02

A subtle trap when formatting Go time.Time values

Let's say that you have a Go time.Time value, tstamp, that you happen to know for sure was collected at 18:00 and some seconds (in your time zone). Now suppose that you use time.Time.Format() on tstamp to get a string form of its hours and minutes (using the format string "15:04"). Will you always get the string '18:00' as the output from formatting tstamp this way?

Perhaps surprisingly, the answer is no; you aren't guaranteed to always get '18:00'. In theory you can get a wide variety of output instead of 18:00, although in practice often the most likely other output is whatever 18:00 in your time zone is in UTC.

What's going on is that Go time.Time values have a Location, also known as a time zone. When you format a time.Time into a string, it uses its location (time zone) to decide how to represent itself. So if tstamp's location is your local time zone, you get the 18:00 that you expect. If tstamp's location has wound up as UTC for some reason, you will get your local 18:00 in UTC, whatever that is. And so on.

Go's time.Now() specifically returns a time.Time that is located in local time, whatever that is, so if you have a timestamp that comes from that it will behave as you expect. However, time.Time values that you get from elsewhere may carry different locations along with them. In particular, if you decode a DateTime value from JSON (in Go), that time value may have carried with it a time zone which will be faithfully propagated into the time.Time value you get. Depending on what generated the JSON, that time zone may be UTC, it may be your local time zone, or it may be something else. This behavior may be surprising if your mental model of decoding a JSON date and time value is that you basically generate a Unix style seconds since epoch value that's neutral on time zones.

(JSON dates and times are commonly represented and decoded in RFC 3339 format or some variant of it.)

The solution is to say what you mean. If you want to format a time.Time value as local time, instead of whatever random time zone it came to you as, you should first make it in local time with .Local(). So instead of tstamp.Format("15:04"), use tstamp.Local().Format("15:04"). Similarly, if you want to format the time in UTC, say that with .UTC(). Unless you're sure you know what you're doing and where your time.Time values came from, using a bare .Format() is probably a mistake.

One potentially surprising place this can come up is in templates. If you have a time.Time value as a template variable, it feels natural to format it with (eg) '{{ .StartsAt.Format "15:04" }}'. But that's probably a mistake unless the time zone for the StartsAt template variable is explicitly documented. Instead, you want '{{ .StartsAt.Local.Format "15:04" }}', which will avoid current and future surprises.

programming/GoTimeHasLocation written at 23:11:49; Add Comment


Page tools: See As Normal.
Search:
Login: Password:
Atom Syndication: Recent Pages, Recent Comments.

This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.