https://utcc.utoronto.ca/~cks/space/blog/programming/GoTimeHasLocation
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.