Datetime formatting in Go
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 forgoNo pun intended 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.
2019-01-21 00:05:45) or timestampsA timestamp is usually an integer value that is equal to the number of seconds that have elapsed since the start of the Unix epoch, which was on 1 January 1970 00:00:00 (e.g.
1548025545). 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 January 2019, 00:00”, “January 21 2019, 12:00 AM” , or “Next Monday around 12 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 some examples of format stringsFeel free to skip ahead to the next section if you’re already familiar with format strings.
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:
Each of the placeholders
Y has a special meaningAnything that is not a placeholder variable simply appears in the output string without being replaced by a value. In this case, that’s the comma and the three spaces between the placeholders:
|l||Name of the day of the week|
|j||Day of the month, without leading zero|
|F||Name of the month|
|Y||Full 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
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:
|Y||Full numeric representation of a year|
|m||Month of the year, with leading zero|
|d||Day of the month, with leading zero|
|H||Hours, using a 24-hour format with leading zeros|
|i||Minutes, with leading zeros|
|s||Seconds, with leading zeros|
Z are preceded by a
\. This is because they are also placeholder characters, and therefore need to be escaped:
|T||Timezone abbreviation, e.g. CST, CET, HKT|
|Z||Timezone offset, in seconds|
If we had omitted the
\, the two characters would have been replaced by values. This results in a malformed datetime string:
Beware of homoglyphs
This example shows what can go wrong if you manually type placeholders rather than copy-pasting them:
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 pivotal moment in Go’s history Which would be weird, since Go wasn’t created until 2009 – but no, it’s what Go developers call the reference time, and it’s pretty clever actually.
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:
Basically, the reference time assigns a number between 0 and 7 to each of the reference time’s components:
|0||Day of the week|
|1||Month of the year|
|2||Day of the month|
|3||Hour of the day|
While the order of the parts seems somewhat arbitraryI don’t think I‘ve ever seen anyone else put the time between the date and year, 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
2019-01-21 00:05:45, which is when this page was last updated.
|Y||%Y||2006||Year, four digits||2019|
|y||%y||06||Year, two digits||19|
|m||%m||01||Month, two digits||01|
|n||-||1||Month, as a number without leading zeros||1|
|M||%b||Jan||Month name, abbreviated||Jan|
|F||%B||January||Month, full name||January|
|d||%d||02||Day of the month, two digits||21|
|j||%e||2||Day of the month, without leading zero||21|
|w||%w||0||Day of the week, as a number||1|
|D||%a||Mon||Day of the week, abbreviated||Mon|
|l||%A||Monday||Day of the week, full name||Monday|
|h||%I||03||Hour, 12-hour notation||12|
|g||%l||3||Hour, without leading zero||12|
|H||%H||15||Hour, 24-hour notation||00|
|i||%M||04||Minute, two digits||05|
|-||-||4||Minute, without leading zero||5|
|s||%S||05||Second, two digits||45|
|-||-||5||Second, without leading zero||45|
|A||%p||PM||AM or PM||AM|
|a||%P||pm||am or pm||am|
|u||%6||.000000||Microseconds (six digits)||739144|
|-||%9||.000000000||Nanoseconds (nine digits)||739144100|
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), 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:
%mfor months, but
- What makes absolutely no sense:
strftimeuses a lowercase
%pfor uppercase AM/PM, but an uppercase
%Pfor lowercase am/pm.
By the way, did you spot the difference between
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.