Chuniversiteit.nl
“Heap, Heap, Array!”

Datetime formatting in Go

Go’s time package takes a novel approach to defining datetime serialisation formats. Is it an improvement over existing alternatives?

Kitchen with a flip clock, a calendar, and an analog wall clock
Go’s unorthodox way of handling datetime formatting may feel somewhat like a flip off to non-American programmers at first, but is pretty okay otherwise.

Most user experience practitioners are familiar with Jakob’s Law of the Internet User Experience, which states that users spend most of their time on other websites. There’s a similar law in programming, the principle of least astonishment, which suggests that one should design systems so that they behave in a way that users expect it to behave. The Go programming language made the bold choice to both principles for its date and time format strings.

Anyone who has spent considerable time programming knows that working with datetimes can be a bit of a chore – even when you ignore the really hard stuff, which is anything that involves daylight saving time or timezones.

Software applications typically process datetimes internally as strings (e.g. 2020-08-21 17:15:23) or (e.g. 1598022923). These formats are fairly consistent among different systems. Input and output formats on the other hand can vary wildly, as different users will prefer different ways to write and view datetimes, like “21 August 2020, 17:00”, “August 21 2020, 05:00 PM”, or “Next Friday around 5 o’clock”.

Ideally software would be smart enough to parse and output any format you throw at it. In some cases that’s possible, but most of the time you’ll have to provide some kind of “hint” that lets the application know which exact format(s) it should use.

Format strings define the date or time format that an application should expect or produce. Format strings are specially constructed pieces of text that contain placeholders, which are replaced by actual days, months, etc. whenever datetime information is processed or displayed.

How others solve the problem

Let’s have a look at .

The PHP library Carbon takes a fairly straightforward approach. In the following example, the line Carbon::now() creates a new Carbon instance that represents the current datetime:

The comment shows what the output would be if we had echoed this. Both the date and time are clearly recognisable, which makes this a pretty decent representation of the time when this page was last generated.

It’s not how dates are usually presented to end-users however. Therefore, we can control how the datetime is displayed using a format string. In this case, we’re using the string l,j F Y:

:

PlaceholderMeaning
lName of the day of the week
jDay of the month, without leading zero
FName of the month
YFull numeric representation of a year

What those meanings are is fairly obvious if you have extensive experience with such functions or just happen to have a cheatsheet opened in a browser tab, but for most people this format string doesn’t convey any meaningful information whatsoever.

Sometimes you’re lucky though and your “users” are actually just other applications. In such cases we can often use a standardised format. Here, we’re formatting the datetime according to the ISO 8601 standard by calling the toIso8601ZuluString method:

We could use a format string to achieve the same effect if we really wanted to, but it wouldn’t be as readable:

Here’s a list of all the placeholders that were used in this format string:

PlaceholderMeaning
YFull numeric representation of a year
mMonth of the year, with leading zero
dDay of the month, with leading zero
HHours, using a 24-hour format with leading zeros
iMinutes, with leading zeros
sSeconds, with leading zeros

The characters T and Z are preceded by a \. This is because they are also placeholder characters, and therefore need to be escaped:

PlaceholderMeaning
TTimezone abbreviation, e.g. CST, CET, HKT
ZTimezone offset, in seconds

If we had omitted the \, the two characters would have been replaced by values. This results in a malformed datetime string:

How Go solves the problem

A major problem with traditional format strings is that it can be rather hard to decipher them. Moreover, mistakes are easily made, but not as easily spotted. It’s therefore not entirely surprising that Go’s developers looked for better ways to format datetimes.

If you browse through Go’s time package documentation you’ll notice that Mon Jan 2 15:04:05 MST 2006 is used fairly often in one form or another. If you hadn’t read it, you’d be forgiven for thinking that it refers to a – but no, it’s what Go developers call the reference time, and it’s actually pretty clever!

Let’s look at what the reference time is and how it’s used.

The reference time

We can present the reference time in a slightly different way to make it easier to see what makes it so special:

Mon Jan 2 15:04:05 MST 2006 = Mon Jan 2 03:04:05PM 2006 -07:00 = 00 01 02 03 04 05 06 07
Each of the components of Go’s reference time has a number assigned to it based on its position in the reference time string – for the most part.

Basically, the reference time assigns a number between 0 and 7 to each of the reference time’s components:

PlaceholderMeaning
0Day of the week
1Month of the year
2Day of the month
3Hour of the day
4Minutes
5Seconds
6Year
7Timezone

While the order of the parts seems somewhat , it does make it easy to create format strings that make sense to readers and don’t require much thought once you get the hang of it.

As the table shows, the placeholder 3 represents hours. So if we want to include the hours in our output, all we need to do is write down the hour in a way that represents the number 3. Want the hour as a simple number? Use 3. Want the hour to be displayed with leading zeros? Add a leading zero: 03. Need a 24-hour format? Use 15, which is three in the afternoon.

That results in format strings that look like this:

I think it’s much more readable and intuitive. You could show one of these format strings to a non-technical person, tell them “Give me the current date and time in this format”, and they’d get it right at the first attempt.

The best of both worlds?

Note that although Go’s placeholders look very different from the ones we saw earlier, you can still reason about them the “old-fashioned” way if you are one of those people who hates change.

The table below provides mappings between the placeholders used by Carbon and Go. For good measure, I’ve also thrown in the placeholders for Ruby’s strftime, a string formatting function that can also found in many other C-like programming languages. All example values are based on the datetime 2020-08-21 17:15:23, which is when this page was last updated.

CarbonstrftimeGoMeaningExample
Y%Y2006Year, four digits2020
y%y06Year, two digits20
m%m01Month, two digits08
n-1Month, as a number without leading zeros8
M%bJanMonth name, abbreviatedAug
F%BJanuaryMonth, full nameAugust
d%d02Day of the month, two digits21
j%e2Day of the month, without leading zero21
w%w0Day of the week, as a number5
D%aMonDay of the week, abbreviatedFri
l%AMondayDay of the week, full nameFriday
h%I03Hour, 12-hour notation05
g%l3Hour, without leading zero5
H%H15Hour, 24-hour notation17
i%M04Minute, two digits15
--4Minute, without leading zero15
s%S05Second, two digits23
--5Second, without leading zero23
A%pPMAM or PMPM
a%Ppmam or pmpm
v%L.000Milliseconds825
u%6.000000Microseconds (six digits)825261
-%9.000000000Nanoseconds (nine digits)825261389
O%z-0700Timezone+0200
P--07:00Timezone+02:00
T%ZMSTTimezoneCEST

It is clear that placeholder usage isn’t very consistent between Carbon and strftime, nor is it consistent within them. There are many cases where strftime’s placeholder differs completely from that of Carbon’s. However, if a library uses both the lowercase and uppercase version of a character for placeholders (e.g. %Y and %y), then the latter version is often used for a more verbose representation of the lowercase placeholder’s value. There are some exceptions to this rule:

  • A somewhat confusing (but still understandable) exception: strftime uses %m for months, but %M for minutes.

  • What makes absolutely no sense: strftime uses a lowercase %p for uppercase AM/PM, but an uppercase %P for lowercase am/pm.

By the way, did you spot the difference between %I and %l?

When things fall flat

There are plenty of reasons to prefer Go’s placeholders over those of Carbon, strftime, and comparable libraries and functions. That doesn’t mean they’re perfect however.

Mistakes are still easily made, and the strange placeholder order is largely to blame for that. For instance, if you accidentally use the placeholder numbers in an order that actually makes sense, you’ll get a formatted string that doesn’t make sense:

Or maybe you didn’t read the documentation at all, because you were just working on some Hugo templates and don’t care about Go. You see these magical datetime strings that “just seem to work”, so you try to use the current date for your own custom format:

Whoops… That doesn’t look right.

Finally, some variables like the day of the year and the week number don’t appear in the reference time, which means you cannot export them using format strings. You can still access the values by calling methods though:

I can live with that.

In fact, these were all the downsides I could think of. None of them is a real dealbreaker to me. Of course, there are some things that I would have liked to see differently, but overall I think it’s pretty nice.

Summary

  1. Most datetime libraries use cryptic format strings to define how a datetime needs to be parsed or displayed

  2. Go uses human-readable format strings, which are much better