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 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.
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.
Let’s have a look at .
The PHP library Carbon takes a fairly
straightforward approach. In the following example, the line
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:
|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:
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.
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 , 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
Want the hour to be displayed with leading zeros? Add a leading zero:
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.
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
string formatting function that can also found in many other C-like programming
languages. All example values are based on the datetime
which is when this page was last updated.
|Y||%Y||2006||Year, four digits||2020|
|y||%y||06||Year, two digits||20|
|m||%m||01||Month, two digits||08|
|n||-||1||Month, as a number without leading zeros||8|
|M||%b||Jan||Month name, abbreviated||Aug|
|F||%B||January||Month, full name||August|
|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||5|
|D||%a||Mon||Day of the week, abbreviated||Fri|
|l||%A||Monday||Day of the week, full name||Friday|
|h||%I||03||Hour, 12-hour notation||05|
|g||%l||3||Hour, without leading zero||5|
|H||%H||15||Hour, 24-hour notation||17|
|i||%M||04||Minute, two digits||15|
|-||-||4||Minute, without leading zero||15|
|s||%S||05||Second, two digits||23|
|-||-||5||Second, without leading zero||23|
|A||%p||PM||AM or PM||PM|
|a||%P||pm||am or pm||pm|
|u||%6||.000000||Microseconds (six digits)||825261|
|-||%9||.000000000||Nanoseconds (nine digits)||825261389|
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
%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
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
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.
Most datetime libraries use cryptic format strings to define how a datetime needs to be parsed or displayed
Go uses human-readable format strings, which are much better