Tech Blog
Working with package time in Go
Working with package time in Go
Overview
At Evergreen Innovations, we have recently been writing back-end systems for our clients in Go. Go has proven itself to be a productive and robust language for these applications, with the time-related functionality available in the standard library being particularly impressive.
In common with most of the Go standard library, the time package is well documented with a number of examples. The official documentation can be found in godoc.
The aim of this blog post is to demonstrate some of the functionality of the time package and highlight a couple of issues we came across when first getting to grips with the package.
Creating Time
The time.Time
type is the workhorse of package time
and, importantly, is time-zone aware. Getting the current time uses the time.Now()
function which returns a time.Time
struct
. The time.Time
struct has many convenience methods, for example
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
day := now.Day()
month := now.Month()
fmt.Println("This is day", day, "of month", month)
}
This is day 27 of month May
To get the month as an integer, we can use the Date
method
...
year, month, day := now.Date()
fmt.Println("Today is in the year", year, " and month", month)
...
The time.Type
type is location aware. If we want to create a specific date-time, the Date
function is used, which has the signature
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time
The final location parameter can either be the constant time.UTC
or be generated from the LoadLocation
function, which takes the timezone name as the only argument. A helpful list of these names can be found here and take the form Europe/London
etc. LoadLocation
also accepts "Local"
and "UTC"
as special cases. For example, to generate a date in Auckland, New Zealand,
func main() {
loc, err := time.LoadLocation("Pacific/Auckland")
if err != nil {
fmt.Println("Could not load location:", err)
return
}
date := time.Date(2019, 5, 5, 11, 0, 0, 0, loc)
fmt.Println("The time in Auckland is", date)
fmt.Println("Or in UTC ", date.UTC())
}
The time in Auckland is 2019-05-05 11:00:00 +1200 NZST
Or in UTC 2019-05-04 23:00:00 +0000 UTC
The package helpfully defines the UTC
method which returns a new time.Time
with the location set to UTC, with the corresponding adjustments to the date and time made.
In order to use time.LoadLocation
, the tz database with the appropriate entry must be on the host system. As with all Go code, therefore, the error should always be checked just in case the database is missing.
Formatting and Parsing
An aspect of the time package which seems slightly confusing at first is the formatting and parsing of time strings. As can be seen in the previous example, the time.Time
type has a String
method but often we want to use a particular format.
Most languages implement time formatting using verbs, similar to those used with fmt.Printf
. For example, using the datetime
module in Python3, the time could be printed with "%H:%M:%S"
. Go takes a very different approach by using predefined formats: the same time string would be created with "15:04:05"
. To create custom strings, we use the Format
method
func main() {
date := time.Now()
fmtStr := "15:04:05"
customStr := date.Format(fmtStr)
fmt.Println("The time is", customStr)
}
The time is 19:00:35
The standard library defines a number of common formats such as RFC3339
which also allow us to work out the individual predefined format elements. These common formats can be used directly with the Format
method
...
fmt.Println("Or in RFC3339:", date.Format(time.RFC3339))
...
Or in RFC3339: 2019-05-27T19:00:35Z
Parsing times from strings uses the same format strings:
func main() {
timeStr := "2019 05 27 11:23:45"
layout := "2006 01 02 15:04:05"
date, err := time.Parse(layout, timeStr)
if err != nil {
fmt.Println("Not able to parse time:", err)
return
}
fmt.Println("date: ", date)
}
date: 2019-05-27 11:23:45 +0000 UTC
If the date is in a particular time zone without the timezone information in the string itself, there is the time.ParseInLocation
function which takes an additional Location
argument which can be created in the same manner as described earlier.
We will explore Unmarshaling and Marshaling custom time formats from/to JSON in a later section.
Time.Duration
The time.Duration
type has a straightforward implementation and makes working with time feel natural. time.Duration
is simply an int64
number of nanoseconds. The beauty arises from Go’s untyped constants which define useful intervals as
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
which permits code such as the following
func sleepFor(duration time.Duration) {
for i := 0; i < 10; i++ {
fmt.Println("Iteration", i)
time.Sleep(duration)
}
}
func main() {
interval := 500 * time.Millisecond
sleepFor(interval)
}
Using time.Duration
as the argument type is much more robust than, say, passing an int
and documenting that this input should be in milliseconds. The standard library functions work extensively with time.Duration
. Some examples of using duration:
func main() {
now := time.Now().UTC()
future := now.Add(5 * time.Hour)
past := now.Add(-11 * time.Minute)
fmt.Println("The time now is", now)
fmt.Println("In the future ", future)
fmt.Println("In the past ", past)
time.Sleep(3000 * time.Millisecond)
elapseLimit := 10 * time.Second
if time.Since(now) > elapseLimit {
fmt.Println("More than", elapseLimit)
} else {
fmt.Println("Less than", elapseLimit)
}
}
The time now is 2019-05-27 19:32:24.663249 +0000 UTC
In the future 2019-05-28 00:32:24.663249 +0000 UTC
In the past 2019-05-27 19:21:24.663249 +0000 UTC
Less than 10s
Note that time.Duration
defines a String
method which also prints duration including units.
The slight wart with time.Duration is trying to create duration from variables which are integers. Go does not allow mixed type arithmetic; therefore the following will not compile
func main() {
interval := 5
time.Sleep(interval * time.Second)
}
with the error
./main.go:8:22: invalid operation: interval * time.Second (mismatched types int and time.Duration)
Instead we need to explicitly convert the variable to a time.Duration
and then apply the correct scale
func main() {
interval := 5
time.Sleep(time.Duration(interval) * time.Second)
}
This explicit conversion feels awkward but, in our experience, is not required all that often and is a small price for the overall utility of the duration type.
(Un)Marshaling Time
We often want to Unmarshal and Marshal times from JSON data. Take the following example
type tsData struct {
Timestamp time.Time `json:"ts"`
Value int `json:"value"`
}
func main() {
input := []byte(`{
"ts": "2019 05 27 12:52:18",
"value": 10
}`)
var data tsData
if err := json.Unmarshal(input, &data); err != nil {
fmt.Println("Could not unmarshal data:", err)
}
fmt.Printf("%+v\n", data)
}
We have defined a type tsData
to represent times series data which has a Timestamp
of type time.Time
and a Value
of type int
. Our simulated JSON input is defined in the input
variable which has a ts
field with a reasonable time format.
Looking back at the documentation, we can see that time.Time
has a method UnmarshalJSON
meaning it implements the Unmarshaler
interface and can be used with json.Unmarshal
.
When we come to run the program, however, we exit with the following error message
Could not unmarshal data: parsing time ""2019 05 27 12:52:18"" as ""2006-01-02T15:04:05Z07:00"": cannot parse " 05 27 12:52:18"" as "-"
This indicates that Unmarshal
is expecting a particular time format which is different to that we have given as input. The documentation for time.UnmarshalJSON
specifies “the time is expected to be a quoted string in the RFC 3339 format”. We therefore need to create our own type that can handle this different format.
Our custom type is called timestamp
which simply embeds a time.Time
. This allows us to use timestamp
in an almost identical manner to a time.Time
throughout the rest of our code.
In order to satisfy the Unmarshaler
interface, we define a single method on timestamp
called UnmarshalJSON
. Note how we can access the anonymous field using its type in the line ts.Time = t
.
const layout = "2006 01 02 15:04:05"
type timestamp struct {
time.Time
}
func (ts *timestamp) UnmarshalJSON(b []byte) error {
// Convert to string and remove quotes
s := strings.Trim(string(b), "\"")
// Parse the time using the layout
t, err := time.Parse(layout, s)
if err != nil {
return err
}
// Assign the parsed time to our variable
ts.Time = t
return nil
}
type tsData struct {
Timestamp timestamp `json:"ts"`
Value int `json:"value"`
}
func main() {
input := []byte(`{
"ts": "2019 05 27 12:52:18",
"value": 10
}`)
var data tsData
if err := json.Unmarshal(input, &data); err != nil {
fmt.Println("Could not unmarshal data:", err)
return
}
fmt.Printf("%+v\n", data)
fmt.Println("Month:", data.Timestamp.Month())
}
Now when we run the code we get the desired output: {Timestamp:2019-05-27 12:52:18 +0000 UTC Value:10} Month: May
Marshaling is achieved by satisfying the json.Marshaler
interface. Note the value receiver (ie not *timestamp
),
...
func (ts timestamp) MarshalJSON() ([]byte, error) {
// The +2 is to take account of the quotation marks
b := make([]byte, 0, len(layout)+2)
// Write the JSON output
b = append(b, '"')
b = ts.AppendFormat(b, layout)
b = append(b, '"')
return b, nil
}
...
Now in func main()
:
...
dataJSON, err := json.Marshal(data)
if err != nil {
fmt.Println("Could not marshal data:", err)
return
}
fmt.Println(bytes.Equal(input, dataJSON))
...
true
The two variables are equal (for this comparison, we have removed all the white space in the input
variable).