DZHG  / How to Parse String as Time in Go

I have been a Java developer for many years. Few months ago, I started learning Golang.

One of the surprising difference between Java and Golang is how to parse a string to a time.

In Java, we’re all familiar with java.time.format.DateTimeFormatter and the magic letters, y, d, M, h, m, s, etc. When I see a string representing a time or date, I’ll automatically map the string to a pattern in my mind.

// the string
String s = "3 Jun 2008 11:05:30";

// the pattern
String pattern = "d MMM yyyy HH:mm:ss";

However, that’s not the case in Golang. The standard time parsing API in package time is:

func Parse(layout, value string) (Time, error)

It accepts a layout as first parameter. What is a layout? The document doesn’t provide a clear definition. It’s definitely not the pattern in Java.

In the time package, there are a few predefined layouts. Such as:

ANSIC       = "Mon Jan _2 15:04:05 2006"
RFC3339     = "2006-01-02T15:04:05Z07:00"

Initially, I would think, OK, the layout just looks like a valid date time value. The Golang library code might be smart enough to “guess” the format of the date time from the “example” (layout). But, wait a second, what is the _ in ANSIC layout? Also, there will be ambiguous formats without any specific context / rules. For example, if I give a “layout” as “01/01/2007”, how the code knows which is month and which is day?

If you read the document carefully, you will know layout is not just an arbitrary date time. There is a rule about the layout. There are some hints in the predefined layouts. For example, the year is all specified as 2006 and the month is all January (or Jan, 01).

The rule is defined / implemented in function nextStdChunk in format.go. We can list all the rules by reading the code:

Layout Chunk Format
January stdLongMonth
Jan stdMonth
Monday stdLongWeekDay
Mon stdWeekDay
MST stdTZ
01 stdZeroMonth
02 stdZeroDay
03 stdZeroHour12
04 stdZeroMinute
05 stdZeroSecond
06 stdYear
002 stdZeroYearDay
15 stdHour
1 stdNumMonth
2006 stdLongYear
2 stdDay
_2 stdUnderDay
_2006 stdLongYear
__2 stdUnderYearDay
3 stdHour12
4 stdMinute
5 stdSecond
PM stdPM
pm stdpm
-070000 stdNumSecondsTz
-07:00:00 stdNumColonSecondsTZ
-0700 stdNumTZ
-07:00 stdNumColonTZ
-07 stdNumShortTZ
Z070000 stdISO8601SecondsTZ
Z07:00:00 stdISO8601ColonSecondsTZ
Z0700 stdISO8601TZ
Z07:00 stdISO8601ColonTZ
Z07 stdISO8601ShortTZ
.0 / .00 / .000 stdFracSecond0
.9 / .99 / .999 stdFracSecond9

Based on the above table, It’s very clear that 01 in the layout will be treated as month and 02 will be the day of month. So, “01/01/2006” won’t work as a layout even though it’s a valid date. The correct layout should be 01/02/2006 (assume I wanted the date format to be MM/dd/yyyy, Java here again).

Once you understand the rule, writing a layout is easy. The only tricky part is the timezone. You must use MST as the timezone name if the string includes the 3-letter timezone abbreviation (this is because MST is UTC-07:00 which matches other timezone layout chunk definitions). To easy remember the layout rule, we just need to keep one timestamp in mind: 01/02/2006 03/04/05 PM MST (in English: January 2nd, 2006, 03:04:05 PM in Mountain Standard Time. It’s a Monday and second day of the year, by the way).

I don’t quite understand the number choices in the layout. It seems just a natural order sequence of 01, 02, 03, 04, 05, 06, 07.

The layout rule doesn’t support a few rarely used cases, such as quarter of year, week of year or week of month, etc. However, it does the job for most of the cases. Also, remembering the layout is just like remembering a datetime. It’s much easier than remembering all the magic letters in Java date time format patterns.

To parse a string as time in Golang, it’s as easy as:

t1, err := time.Parse(time.RFC3339, "2020-08-19T15:24:39-07:00")
if err != nil {
	fmt.Println(err.Error())
}
fmt.Println(t1)

t2, err := time.Parse("01/02/2006 15:04:05 -07:00", "08/19/2020 15:24:39 -07:00")
if err != nil {
	fmt.Println(err.Error())

}
fmt.Println(t2)

You will get same output:

2020-08-19 15:24:39 -0700 PDT
2020-08-19 15:24:39 -0700 PDT

One last catch when using time.Parse is that, we shall avoid using named timezone abbreviations, such as, PDT, MST, etc. The method internally always uses local timezone for the final date time even though the offset is calculated by the timezone abbreviation. You will get different result when running below code at different locations (or with different system timezones):

t, err := time.Parse("01/02/2006 15:04:05 MST", "08/19/2020 15:24:39 PDT")
if err != nil {
	fmt.Println(err.Error())

}
fmt.Println(t)