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.

Photo by Bob Clark from Pexels

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.

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.

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.

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

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.