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.
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
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
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.
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
To keep up to date, there is a mailing list you can join on the official IANA page which publishes the
Big thanks to Filip Reškov for helping me trace the issue.