Overcoming Go’s summer-time localization issues
If you are using local timezone formatting in your Go project, you might have experienced a sudden issue with incorrectly formatted dates over the past few days. Despite summer time ending in Europe last weekend, Go still formats the date as if it was active. So for the location of Europe/Zagreb, it still shows up as CEST instead of CET.
What happened?
The background story is already covered in great detail here. The short version is that a few months ago there was a change in the way how timezone data is built to make definitions more compact. At the same time, Go had a bug on how it handles those slimmer definitions. While fix is going to land in future versions, here is how you can mitigate the issue meanwhile and support the older versions that won’t be fixed.
If you are only using the UTC dates and do not format the dates using time.LoadLocation
you are unlikely to be affected. Although, you might still be impacted if some dependency is using it.
How to reproduce it?
To make sure we have solved the issue, let’s take the following example, an app that just outputs the localized date in the Zagreb timezone running inside a Docker container.
If we build it as a multi-stage Docker image with the latest alpine
and tzdata
(at the time of writing it fetches 2020c
which is outdated).
When we run the image we can see that output is not the expected one. The first of November should be formatted as 13:00:00 in CET, not the CEST, since summer-time ended on October 25th, 2020.
Europe/Zagreb 2020-11-01 14:00:00 CEST
How to fix it?
There are multiple ways to go about it, but given we are using Docker I am going to focus on the solution that is applicable to all versions of Go and easily implemented even on older images.
As explained in the source code, Go will look for time zone definitions in an environment variable named ZONEINFO
. It then fallbacks to the default Unix locations, followed by definitions bundled within Go SDK. By default alpine
is not distributed with either of those, that is why we had to explicitly install them using apk add -U tzdata
otherwise our app would panic.
Building the latest tzdata
Since one of the root causes of the issue is the slim
version of tzdata
, based on the script that is available in the latest version of Go repo, we can build our own version of the latest definitions in fat
format by adding the build argument ZFLAGS="-b fat"
.
Make sure to have build-essentials installed, and you can then run it using bash update.bash
to get the definitions.
Injecting the latest tzdata into Docker image
Once built we get a single zip zoneinfo.zip
containing all the definitions. The only remaining step is to add it to our runner image and tell go to use it.
If we run it again, we should see that it now works correctly.
Europe/Zagreb 2020-11-01 13:00:00 CET
Automating the process
We can go a step further and build the tzdata
at the same time we are building the app itself. In practice, you will want to build a custom base image that includes both the tzdata
as well as ca-certificates
and other common dependencies you might have instead of doing it on each separate Go application.
In case you are not packing anything else into the image, you likely won’t need the apk add -U tzdata
anymore as Go will use the ones we built.
Future considerations
While future versions of Go are going to fix the issue, there is still good reason to support the latest version of time zone definitions as EU is planning to end the daylight savings. This fix allows supporting the older versions of Go while still using the latest tzdata
definitions.
To keep up to date, there is a mailing list you can join on the official IANA page which publishes the tzdata
releases.
Big thanks to Filip Reškov for helping me trace the issue.