Datetime formatting in Go

Close-up of a disassembled flip 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.
Original photo by Reinraum.

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. 2018-09-16 16:42:40) 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. 1537108960). 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 “16 September 2018, 16:00”, “September 16 2018, 04:00 PM” , or “Next Sunday around 4 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:

Carbon::now(); // 2018-09-16 16:42:40

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:

Carbon::now()->format('l, j F Y'); // Sunday, 16 September 2018

Each of the placeholders l, j, F, and 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:

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:

Carbon::now()->toIso8601ZuluString(); // 2018-09-16T16:42:40Z

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

Carbon::now()->format('Y-m-d\TH:i:s\Z'); // 2018-09-16T16:42:40Z

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:

Carbon::now()->format('Y-m-dTH:i:sZ'); // 2018-09-16CEST16:42:400

Beware of homoglyphs

This example shows what can go wrong if you manually type placeholders rather than copy-pasting them:

Carbon::now()->format('l'); // Sunday
Carbon::now()->format('1'); // 1
Carbon::now()->format('I'); // 0
Carbon::now()->format('i'); // 42

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:

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:

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 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:

time.Now().Format("2006-01-02T15:04:05+07:00") // 2018-09-16T16:42:40+02:00
time.Now().Format("Monday, 2 January 2006")    // Sunday, 16 September 2018
time.Now().Format("It’s 3 o’clock right now")  // It’s  4 o’clock right now

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 2018-09-16 16:42:40, which is when this page was last updated.

Carbon strftime Go Meaning Example
Y %Y 2006 Year, four digits 2018
y %y 06 Year, two digits 18
m %m 01 Month, two digits 09
n - 1 Month, as a number without leading zeros 9
M %b Jan Month name, abbreviated Sep
F %B January Month, full name September
d %d 02 Day of the month, two digits 16
j %e 2 Day of the month, without leading zero 16
w %w 0 Day of the week, as a number 0
D %a Mon Day of the week, abbreviated Sun
l %A Monday Day of the week, full name Sunday
h %I 03 Hour, 12-hour notation 04
g %l 3 Hour, without leading zero 4
H %H 15 Hour, 24-hour notation 16
i %M 04 Minute, two digits 42
- - 4 Minute, without leading zero 42
s %S 05 Second, two digits 40
- - 5 Second, without leading zero 40
A %p PM AM or PM PM
a %P pm am or pm pm
v %L .000 Milliseconds 328
u %6 .000000 Microseconds (six digits) 328454
- %9 .000000000 Nanoseconds (nine digits) 328454356
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:

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:

time.Now().Format("2001-02-03 04:05:06") // 16009-16-16 42:40:18 (=2018-09-16 16:42:40)

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:

time.Now().Format("Sunday, 10 June 2018") // Sunday, 60 June 10068

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:

time.Now().YearDay() // 259
time.Now().ISOWeek() // 2018 37 (these are actually two separate return values)

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