Golang Project Structure

Tips and tricks for writing and structuring Go code

Handling Times, Dates and Durations of Time

Language

  • unknown

by

Unlike some other programming languages, Go offers really good native support for working with durations, dates and times.

A traditional pocketwatch, used to keep track of the time.
Physical clocks and pocketwatches have been used for hundreds of years to keep track of the time.

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.

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.

MethodResultResult Type
customTime.Year()2001int
customTime.YearDay()319int
customTime.Month()"November"time.Month
customTime.Weekday()"Thursday"time.Weekday
customTime.Day()15int
customTime.Hour()18int
customTime.Minute()57int
customTime.Second()24int
customTime.Nanosecond()521186359int
customTime.Unix()1005850644int64
customTime.UnixNano()1005850644521186359int64
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):

MethodResult
customTime.String()2001-11-15 18:57:24.521186359 +0000 UTC
customTime.Format(time.UnixDate)Thu Nov 15 18:57:24 UTC 2001
customTime.Format(time.RubyDate)"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:

MethodResultResult Type
customDuration.Hours()678838.0009505246float64
customDuration.Minutes()4.0730280057031475e+07float64
customTime.Seconds()2.443816803421889e+09float64
customTime.Milliseconds()2443816803421int64
customTime.Microseconds()2443816803421888int64
customTime.Nanoseconds()2443816803421888700int64

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.

Leave a Reply

Your email address will not be published. Required fields are marked *