Handling Times, Dates and Durations of Time
Language
- unknown
by James Smith (Golang Project Structure Admin)
Unlike some other programming languages, Go offers really good native support for working with durations, dates and times.
We will look at how durations, dates and times are represented in Go, before exploring some of the various methods and functions that can be used to modify, parse or format them.
Table of Contents
Representing Durations
A duration can be defined as an interval of time. In other words, a duration quantifies how much time has elapsed between two distinct points in time. It is a measure of how big, or small, the gap between the two times was.
Durations are stored in the following type defined by the "time"
package within the standard library:
package time
type Duration int64
It is a count of nanoseconds between two discrete time intervals. The use of a 64-bit integer implies that largest representable duration is approximately 290 years.
This should be enough for most uses, but if you need a duration of centuries or even millennia, then you will have to create a custom type that can hold this.
If you are happy to accept less fidelity, then perhaps you could simply redefine an int64
as a duration of years, like so:
package main
import "fmt"
type DurationInYears int64
func main() {
const cretaceousPaleogeneExtinctionEvent DurationInYears = 6.6e7
fmt.Printf(
"The dinosaurs became extinct around %d years ago.\n",
cretaceousPaleogeneExtinctionEvent,
)
}
If you represent years with a 64-bit integer, then you can store a maximum duration of around nine quintillion years (or around double that if you use an unsigned 64-bit integer).
Since physicists estimate that the universe began only 13.8 billion years ago, that should be more than enough!
In the example above, the cretaceousPaleogeneExtinctionEvent
variable holds a value of sixty-six million, which was roughly how long ago in years that dinosaurs were destroyed by a huge asteroid.
(In fact, more than 75% of all plant and animal species on Earth were made extinct at that time, and it is known as the Cretaceous–Paleogene extinction event, since it has come to mark the boundary between two major geological epochs.)
Even though it is possible to define your own type for longer durations, unless there is a pressing need to do so, you should generally use the time.Duration
type provided by the standard library, since that will enable you to interact better with code written by others.
Representing Dates and Times
The custom type shown below, which is also in the "time"
package, is used for storing dates and times:
package time
type Time struct {
wall uint64
ext int64
loc *Location
}
It isn’t necessary to understand the fine details behind the internal workings of the time.Time
struct, since none of its fields are exported — but wall
usually stores a count of seconds and ext
holds nanoseconds.
Notice that every time.Time
variable also has an associated location. This is used to determine the correct timezone. If the loc
field is set to nil, then a UTC timezone will be assumed.
Programs that use the time.Time
struct will generally store and pass it around as a value, rather than a pointer. This is because there is so little overhead in copying the small number of fields that it contains.
The maximum year that can currently be represented with full nanosecond accuracy is 2157 — while the minimum year is 1885. However, very much bigger and smaller times can be represented, if you’re prepared to lose some degree of accuracy at the nanosecond level.
Getting the Current Time
Helpfully, the time.Now
function will return a time.Time
value set to your computer’s current time:
package main
import (
"fmt"
"time"
)
func main() {
currentTime := time.Now().UTC()
fmt.Printf("%s\n", currentTime)
}
As shown above, the UTC method assures that the timezone is standardized. This can be especially useful if you are sharing dates and times between different machines, perhaps running in different parts of the world, if you are transmitting and receiving data over an internet connection.
The method UTC
can be called on any time.Time
variable in order to convert its location to the Universal Time Coordinated (UTC) standard.
The String
method is called on the time.Time
struct automatically when it is printed by the fmt.Printf
function, enabling an attractive and human-readable representation:
2022-10-15 15:27:43.4604728 +0000 UTC
You can see above that I initially ran the previous example code in the afternoon in the middle of October 2022.
Working With a Custom Date and Time
If you already have a date or time — either in your head or stored in another format — that you want to import into your Go code, then you will need to use the time.Date
function in order to populate a time.Time
struct with the necessary information:
package main
import (
"fmt"
"time"
)
func main() {
customTime := time.Date(2001, 11, 15, 18, 57, 24, 521186359, time.UTC)
fmt.Println(customTime)
}
In the example above, each of the arguments to the time.Date
function corresponds to a specific unit of time. For example, 2001
is the year, 11
is the month, 15
is the date, 18
is the hour, 57
is the minute, 24
is the second and 521186359
is the number of nanoseconds.
The final argument passed to time.Date
is a pointer to a time.Location
struct that represents the timezone.
Methods Available on Time Variables
The table below contains some of the most important methods that can be called on a time.Time
variable. Also, included are the return types for each function and the result we would be given if we were to call each method on the customTime
variable defined in the previous example.
Method | Result | Result Type |
---|---|---|
customTime.Year() | 2001 | int |
customTime.YearDay() | 319 | int |
customTime.Month() | "November" | time.Month |
customTime.Weekday() | "Thursday" | time.Weekday |
customTime.Day() | 15 | int |
customTime.Hour() | 18 | int |
customTime.Minute() | 57 | int |
customTime.Second() | 24 | int |
customTime.Nanosecond() | 521186359 | int |
customTime.Unix() | 1005850644 | int64 |
customTime.UnixNano() | 1005850644521186359 | int64 |
customTime.Location() | "UTC" | *time.Location |
Note that time.Month
and time.Weekday
are custom types used to represent a given month of the year or a given day of the week respectively. They are each defined as integer types, but they both also have String
methods that will provide a human-readable representation.
For example, calling the String method on a time.Weekday
variable that has the value 3
will give "Wednesday"
, because numerical values between 0
and 6
inclusive represent the days of the week between "Sunday"
and "Saturday"
inclusive.
On the other hand, the value 0
has no valid meaning for a time.Month
variable, because the months between "January"
and "December"
inclusive are represented by numerical values between 1
and 12
inclusive.
Printing a Time in a Specified Format
The table below shows the various ways that a time.Time
variable can be converted to a string (so that it can be displayed to a human user, for example):
Method | Result |
---|---|
customTime.String() | 2001-11-15 18:57:24.521186359 +0000 UTC |
customTime.Format(time.UnixDate) | Thu Nov 15 18:57:24 UTC 2001 |
| "Thu Nov 15 18:57:24 +0000 2001" |
customTime.Format(time.ANSIC) | "Thu Nov 15 18:57:24 2001" |
customTime.Format(time.RFC822) | 15 Nov 01 18:57 UTC |
customTime.Format(time.RFC822Z) | 15 Nov 01 18:57 +0000 |
customTime.Format(time.RFC850) | Thursday, 15-Nov-01 18:57:24 UTC |
customTime.Format(time.RFC1123) | Thu, 15 Nov 2001 18:57:24 UTC |
customTime.Format(time.RFC1123Z) | Thu, 15 Nov 2001 18:57:24 +0000 |
customTime.Format(time.RFC3339) | 2001-11-15T18:57:24Z |
customTime.Format(time.RFC3339Nano) | 2001-11-15T18:57:24.521186359Z |
customTime.Format(time.Kitchen) | "6:57PM" |
Calling the String
method will return a default formatting. However, calling the Format
method with one of the constants shown above allows you to specify a particular formatting.
The format returned by the String
method can very easily be parsed by other programming languages, in order to recreate the entire date, as shown by the JavaScript example below:
const golangDateString = "2001-11-15 18:57:24.521186359 +0000 UTC";
const parsedDate = new Date(golangDateString);
console.log(date.getUTCMinutes()) // 57
Parsing a Time From a String in a Specified Format
The example below shows how a time.Time
variable can be populated with information read from a string:
package main
import (
"fmt"
"time"
)
func main() {
const year = 2014
const month = 5
const day = 4
const hour = 16
const minute = 54
const second = 21
timeString := fmt.Sprintf(
"%04d-%02d-%02dT%02d:%02d:%02dZ",
year, month, day, hour, minute, second,
)
timeValue, err := time.Parse(time.RFC3339, timeString)
if err != nil {
panic(err)
}
fmt.Println(timeValue)
}
The time.Parse
function takes a formatting constant as its first argument that defines how the time stored in the string passed as the second argument should be laid out.
Calculating the Duration Between Two Times
The example below shows how to calculate the duration that would have elapsed between two different time.Time
variables:
package main
import (
"fmt"
"time"
)
func main() {
historicTime := time.Date(1945, 5, 7, 02, 41, 00, 0, time.UTC)
currentTime := time.Now().UTC()
currentDuration := currentTime.Sub(historicTime)
fmt.Println(currentDuration)
}
We first declare a time.Time
variable using the time.Date
function, setting historicTime
to forty-one minutes past two in the morning on the seventh of May 1945 (which happens to have been the time when the Second World War came to an end in Europe with the surrender of Germany).
The variable currentTime
is set, as its name suggests, to the present time when the program is running. Both historicTime
and currentTime
use the time.UTC
timezone (technically known as a time.Location
), since this represents a standard that can be used to compare different times across the world.
However, it would generally be better, in cases like this, to use the Since
function shown below, since that automatically gets the current time and performs the calculation to work out the duration elapsed since the time passed as an argument:
package main
import (
"fmt"
"time"
)
func main() {
historicTime := time.Date(1945, 5, 7, 18, 41, 02, 0, time.UTC)
currentDuration := time.Since(historicTime)
fmt.Println(currentDuration)
}
Methods Available on Duration Variables
Finally, the following table contains some of the methods that can be called on a time.Duration
variable in order to access the duration expressed in the chosen units:
Method | Result | Result Type |
---|---|---|
customDuration.Hours() | 678838.0009505246 | float64 |
customDuration.Minutes() | 4.0730280057031475e+07 | float64 |
customTime.Seconds() | 2.443816803421889e+09 | float64 |
customTime.Milliseconds() | 2443816803421 | int64 |
customTime.Microseconds() | 2443816803421888 | int64 |
customTime.Nanoseconds() | 2443816803421888700 | int64 |
Notice that the result returned by each of these methods is just a different way of expressing the same duration: the only thing that has changed is the units.
If these methods didn’t exist, it would have been possible to calculate them with some basic arithmetic as shown below:
package main
import (
"fmt"
"time"
)
func main() {
currentDuration := time.Since(time.Date(1945, 5, 7, 18, 41, 02, 0, time.UTC))
currentDurationInNanoseconds := float64(currentDuration)
currentDurationInMicroseconds := currentDurationInNanoseconds / 1e3
currentDurationInMilliseconds := currentDurationInMicroseconds / 1e3
currentDurationInSeconds := currentDurationInMilliseconds / 1e3
currentDurationInMinutes := currentDurationInSeconds / 60
currentDurationInHours := currentDurationInMinutes / 60
fmt.Println("Each of the following durations is equivalent to the others:")
fmt.Printf("\t%.0f nanoseconds\n", currentDurationInNanoseconds)
fmt.Printf("\t%.0f microseconds\n", currentDurationInMicroseconds)
fmt.Printf("\t%.0f milliseconds\n", currentDurationInMilliseconds)
fmt.Printf("\t%.0f seconds\n", currentDurationInSeconds)
fmt.Printf("\t%.0f minutes\n", currentDurationInMinutes)
fmt.Printf("\t%.0f hours\n", currentDurationInHours)
}
Notice how we simply convert the initial time.Duration
value to a float64
representation when initializing the currentDurationInNanoseconds
variable, since every time.Duration
value is internally just a count of nanoseconds.