Learn How to Handle Time In Golang

99e178071ff04a758df4210c2e330fc8.png~tplv-6bxrjdptv7-image
Handling time is a common task for programmers. In Go, the standard library time provides the necessary capabilities.

This article will introduce some important functions and methods in the time package, hoping to help those who often need to deal with time-related issues in Go.

Handle Time Zones

In programming, we often encounter the issue of an eight-hour time difference. This is caused by differences in time zones. To better handle them, we need to understand several time definition standards.

GMT (Greenwich Mean Time) is based on the Earth’s rotation and revolution to calculate time. It defines noon as the time when the sun passes through the Royal Greenwich Observatory in the suburbs of London, UK. GMT was the former world standard time.

UTC (Coordinated Universal Time) is more precise than GMT, calculated based on atomic clocks. In situations where precision to the second is not required, UTC can be considered equivalent to GMT. UTC is the current world standard time.

From the Prime Meridian at Greenwich, going east is positive, going west is negative. The globe is divided into 24 standard time zones, with neighboring time zones differing by one hour.

1
2
3
4
5
6
7
8
9
10
package main

import (
"fmt"
"time"
)

func main() {
fmt.Println(time.Now())
}

In mainland China, the standard time used is in the GMT+8 time zone, known as China Standard Time (CST).

1
2
$ go run main.go
2022-07-17 16:37:31.186043 +0800 CST m=+0.000066647

This is the result under the default time zone, and +0800 CST is indicated in the time.Now() printout.

Suppose we are in the Los Angeles time zone in the United States, what result do we get?

1
2
$ TZ="America/Los_Angeles" go run main.go
2022-07-17 01:39:12.391505 -0700 PDT m=+0.000069514

As seen, the result at this time is -0700 PDT, which is Pacific Daylight Time. Due to the time zone difference, the results of the two executions differ by 15 hours.

Note that when using Docker containers, the system’s default time zone is UTC time (0 time zone), which is eight hours behind Beijing time as we need, leading to the classic scenario of the eight-hour time difference problem.

Strategies for dealing with time zone issues can be found in detail in the loading logic of the initLocal() function in src/time/zoneinfo_unix.go. For example, you can solve it by specifying the environment variable TZ or modifying the /etc/localtime file.

Because time zone issues are very important, they are discussed in the first part of the article. Let’s now move on to the usage of the time package.

Time Instant time.Time

The core object in the time package is the time.Time struct. Its definition, used to represent a specific moment in time, is as follows:

1
2
3
4
5
6
7
8
type Time struct {
// wall and ext encode the wall time seconds,
// wall time nanoseconds, and optional monotonic
// clock reading in nanoseconds.
wall uint64
ext int64
loc *Location
}

In computer time handling, two types of clocks are mainly involved:

  • Wall clock, also known as clock time, used to represent specific dates and times.
  • Monotonic clocks, which always guarantee that time moves forward without the issue of wall clock rollback, making them suitable for measuring durations.

The wall and ext fields are used to record wall clock and monotonic clock times, with nanosecond precision. The bits of these fields are associated with specific information such as years, months, days, hours, minutes, and seconds.

The loc field records the time zone location. When loc is nil, it defaults to UTC time.

Because time.Time is used to represent time instants with nanosecond precision, it is typically stored and passed as a value in programs, rather than a pointer.

That is, in time variables or struct fields, we should use time.Time rather than *time.Time.

Getting time.Time

We can get the current local time using the Now function:

1
func Now() Time {}

Or, using the Date function, we can get a specified time based on the year, month, day, and other parameters, along with the time zone:

1
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time {}
Converting Timestamps

In the computer world, UTC time 0 on January 1, 1970, is considered Unix time 0. To convert a time instant into a Unix timestamp, we calculate the number of seconds, microseconds, etc., elapsed from Unix time 0 to the specified instant.

1
2
3
4
func (t Time) Unix() int64       // Seconds since Unix time 0
func (t Time) UnixMicro() int64 // Microseconds since Unix time 0
func (t Time) UnixMilli() int64 // Milliseconds since Unix time 0
func (t Time) UnixNano() int64 // Nanoseconds since Unix time 0
Getting Basic Fields
1
2
3
4
5
6
7
8
9
10
11
12
13
t := time.Now()
fmt.Println(t.Date()) // 2022 July 17
fmt.Println(t.Year()) // 2022
fmt.Println(t.Month()) // July
fmt.Println(t.ISOWeek()) // 2022 28
fmt.Println(t.Clock()) // 22 21 56
fmt.Println(t.Day()) // 17
fmt.Println(t.Weekday()) // Sunday
fmt.Println(t.Hour()) // 22
fmt.Println(t.Minute()) // 21
fmt.Println(t.Second()) // 56
fmt.Println(t.Nanosecond()) // 494313000
fmt.Println(t.YearDay()) // 198

Duration time.Duration

time.Duration represents the time elapsed between two time.Time instants. It uses an int64 to represent the count of nanoseconds, allowing for approximately 290 years of representation.

1
2
3
4
// A Duration represents the elapsed time between two instants
// as an int64 nanosecond count. The representation limits the
// largest representable duration to approximately 290 years.
type Duration int64

In Go, time.Duration is simply a number in nanoseconds. If a duration is equal to 1000000000, it represents 1 second, or 1000 milliseconds, or 1000000 microseconds, or 1000000000 nanoseconds.

For example, the duration between two time instants separated by 1 hour can be calculated as:

1
1*60*60*1000*1000*1000

The time package defines constant values for these durations:

1
2
3
4
5
6
7
8
9
10
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond

= 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)

Additionally, time.Duration provides methods to get values at various time granularities:

1
2
3
4
5
6
func (d Duration) Nanoseconds() int64   // Nanoseconds
func (d Duration) Microseconds() int64 // Microseconds
func (d Duration) Milliseconds() int64 // Milliseconds
func (d Duration) Seconds() float64 // Seconds
func (d Duration) Minutes() float64 // Minutes
func (d Duration) Hours() float64 // Hours

Time Calculation

After learning about time instants and durations, let’s see how to perform time calculations.

1
func (t Time) Add(d Duration) Time {}
  • Add adds or subtracts (positive d means addition, negative d means subtraction) a time.Duration to a time.Time. We can add or subtract durations of specific nanosecond levels to a specific instant in time.
1
func (t Time) Sub(u Time) Duration {}
  • Sub returns the duration between two time instants.
1
func (t Time) AddDate(years int, months int, days int) Time {}
  • AddDate adds or subtracts values based on the year, month, and day dimensions to a time.Time.

Of course, calculating based on the current time instant time.Now() is the most common requirement. Therefore, the time package also provides the following convenient time calculation functions:

1
func Since(t Time) Duration {}

Since is a shortcut for time.Now().Sub(t).

1
func Until(t Time) Duration {}

Until is a shortcut for t.Sub(time.Now()).

Usage Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
t := time.Now()
fmt.Println(t) // 2022-07-17 22:41:06.001567 +0800 CST m=+0.000057466

// Adding 1 hour to the time
fmt.Println(t.Add(time.Hour * 1)) // 2022-07-17 23:41:06.001567 +0800 CST m=+3600.000057466
// Adding 15 minutes
fmt.Println(t.Add(time.Minute * 15)) // 2022-07-17 22:56:06.001567 +0800 CST m=+900.000057466
// Adding 10 seconds
fmt.Println(t.Add(time.Second * 10)) // 2022-07-17 22:41:16.001567 +0800 CST m=+10.000057466

// Subtracting 1 hour
fmt.Println(t.Add(-time.Hour * 1)) // 2022-07-17 21:41:06.001567 +0800 CST m=-3599.999942534
// Subtracting 15 minutes
fmt.Println(t.Add(-time.Minute * 15)) // 2022-07-17 22:26:06.001567 +0800 CST m=-899.999942534
// Subtracting 10 seconds
fmt.Println(t.Add(-time.Second * 10)) // 2022-07-17 22:40:56.001567 +0800 CST m=-9.999942534

time.Sleep(time.Second * 5)
t2 := time.Now()
// Calculating the duration from t to t2
fmt.Println(t2.Sub(t)) // 5.004318874s
// Time after 1 year
t3 := t2.AddDate(1, 0, 0)
// Calculating the duration from t to the current time
fmt.Println(time.Since(t)) // 5.004442316s
// Calculating the duration from now to next year
fmt.Println(time.Until(t3)) // 8759h59m59.999864s

Formatting Time

In other languages, a universal time template is typically used to format time. For example, in Python, %Y represents year, %m represents month, %d represents day, and so on.

However, Go is different. It uses a fixed time (it’s important to note that using other times is not allowed) as the layout template, and this fixed time is the birth time of the Go language.

1
Mon Jan 2 15:04:05 MST 2006

Formatting time involves two conversion functions:

1
func Parse(layout, value string) (Time, error) {}
  • Parse converts a time string to a time.Time object based on the layout it can correspond to.
1
func (t Time) Format(layout string) string {}
  • Format converts a time.Time object to a time string based on the given layout.
Example
1
2
3
4
5
6
7
8
const (
layoutISO = "2006-01-02"
layoutUS = "January 2, 2006"
)
date := "2012-08-09"
t, _ := time.Parse(layoutISO, date)
fmt.Println(t) // 2012-08-09 00:00:00 +0000 UTC
fmt.Println(t.Format(layoutUS)) // August 9, 2012

In the time package, Go provides some predefined layout template constants that can be directly used.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const (
Layout = "01/02 03:04:05PM '06 -0700" // The reference time, in numerical order.
ANSIC = "Mon Jan _2 15:04:05 2006"
UnixDate = "Mon Jan _2 15:04:05 MST 2006"
RubyDate = "Mon Jan 02 15:04:05 -0700 2006"
RFC822 = "02 Jan 06 15:04 MST"
RFC822Z = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
RFC850 = "Monday, 02-Jan-06 15:04:05 MST"
RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST"
RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
RFC3339 = "2006-01-02T15:04:05Z07:00"
RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
Kitchen = "3:04PM"
// Handy time stamps.
Stamp = "Jan _2 15:04:05"
StampMilli = "Jan _2 15:04:05.000"
StampMicro = "Jan _2 15:04:05.000000"
StampNano

= "Jan _2 15:04:05.000000000"
)

Here’s a table of optional layout parameters:

1
2
3
4
5
6
7
8
9
10
11
12
13
Year            06/2006
Month 01/1/Jan/January
Day 02/2/_2
Weekday Mon/Monday
Hour 03/3/15
Minute 04/4
Second 05/5
Milliseconds .000/.999
Microseconds .000000/.999999
Nanoseconds .000000000/.999999999
am/pm PM/pm
Timezone MST
Timezone offset -0700/-07/-07:00/Z0700/Z07:00

Timezone Conversion

At the beginning of the article, we discussed timezone issues. If in your code, you need to get the result of the same time.Time in different time zones, you can use its In method.

1
func (t Time) In(loc *Location) Time {}

It’s straightforward to use. Let’s see an example code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
now := time.Now()
fmt.Println(now) // 2022-07-18 21:19:59.9636 +0800 CST m=+0.000069242

loc, _ := time.LoadLocation("UTC")
fmt.Println(now.In(loc)) // 2022-07-18 13:19:59.9636 +0000 UTC

loc, _ = time.LoadLocation("Europe/Berlin")
fmt.Println(now.In(loc)) // 2022-07-18 15:19:59.9636 +0200 CEST

loc, _ = time.LoadLocation("America/New_York")
fmt.Println(now.In(loc)) // 2022-07-18 09:19:59.9636 -0400 EDT

loc, _ = time.LoadLocation("Asia/Dubai")
fmt.Println(now.In(loc)) // 2022-07-18 17:19:59.9636 +0400 +04

Conclusion

In general, the functions and methods provided by the time package for time processing meet our usage needs.

Interestingly, Go’s time formatting conversion must adopt Go’s birth time. It’s quite self-centered.