Go’s time package takes a novel approach to defining datetime serialisation formats. Is it an improvement over existing alternatives?
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:
:
Placeholder
Meaning
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
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:
Placeholder
Meaning
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
The characters T and Z are preceded by a \. This is because they are also
placeholder characters, and therefore need to be escaped:
Placeholder
Meaning
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:
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:
Basically, the reference time assigns a number between 0 and 7 to each of the
reference time’s components:
Placeholder
Meaning
0
Day of the week
1
Month of the year
2
Day of the month
3
Hour of the day
4
Minutes
5
Seconds
6
Year
7
Timezone
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.
Carbon
strftime
Go
Meaning
Example
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
v
%L
.000
Milliseconds
825
u
%6
.000000
Microseconds (six digits)
825261
-
%9
.000000000
Nanoseconds (nine digits)
825261389
O
%z
-0700
Timezone
+0200
P
-
-07:00
Timezone
+02:00
T
%Z
MST
Timezone
CEST
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
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