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)