Have you ever used time.Now()
or ran a method like createdAt.Format(...)
?
The time
package is a staple in many Go programs, often used for core functionality, logging, or other metadata.
When diving into its documentation, you’ll find it’s thorough but focusses on niche topics like “Monotonic Clocks”. It doesn’t really provide beginners with a clear introduction to the package.
This might raise questions like:
- What exactly is the “instant in time” mentioned in the docs?
- How does
time.Time
“represent” such an instant? - Is a
time.Location
just another term for “time zone”?
This article aims to answer such questions and (hopefully!) provide you with a solid mental model of the time
package.
Representing instants in time
The docs frequently mention the terms “instant in time” or “time instant”, but what do they mean?
I think of it as a single moment that occurs simultanously across the globe, regardless of time zones, local representations, or other complexities.
This can be visualized as a vertical line:
Now this is not very useful if we have no way to relate this time instant to other time instants, making communication and coordination rather difficult.
We need a way to represent the time instant on some kind of timeline. For example:
- A timeline of Julian Days.
- Timeline of hours since the start of the Eurovision Song Contest Final in 2023.
- The Coordinated Universal Time (UTC) timeline, used to coordinate time worldwide.
The point where our vertical line intersects with a timeline is the representation of the time instant on that timeline. Our time instant is represented as:
- The
2460293.25
th Julian Day. 5159
hours since the start of Eurovision Song Contest Final in 2023.18:00 on 14th of December 2023
in UTC time.
As you might have deduced from the examples, we can come up with as many timelines as we would like (some more useful than others). A time instant can have infinite representations.
As a consequence, the actual value used to represent a time instant is useless if we don’t know what timeline it belongs to.
For example, if someone told you:
My birthday is on the
14th
.
You’d need to know what timeline they are referring to for it to make sense: The 14th
of what?
So how does this all relate to our Go types time.Time
and time.Location
?
time.Time and time.Location explained
When we check the docs for time.Time
we read:
A Time represents an instant in time with nanosecond precision.
Like the representations we saw earlier, every time.Time
value is another representation of a time instant.
And again, we need a timeline for this value to have meaning. Internally, every time.Time
value has a pointer to a time.Location
struct.
The time.Location
is what provides a timeline according to a set of rules. The simplest locations just provide a single timeline.
Let’s take a look at an example.
Suppose we have a time instant that is represented by the time.Time
value 13:37:00 19 December 2023
that references the time.UTC
location.
The same time instant can also be represented by a time.Time
value that references a different location, say, one that is an hour east of UTC:
package main
import (
"fmt"
"time"
)
func main() {
// create a *time.Location one hour (3600 seconds) east of UTC.
eastOfUTC := time.FixedZone("UTC+1", 60*60)
// create two time representations of the same time instant.
t1 := time.Date(2023, 12, 19, 13, 37, 0, 0, time.UTC)
t2 := time.Date(2023, 12, 19, 14, 37, 0, 0, eastOfUTC)
fmt.Printf("t1: %v\n", t1)
fmt.Printf("t2: %v\n", t2)
fmt.Printf("same instant? %v\n", t1.Equal(t2))
}
We can visualize the two time.Time
values on the two timelines as follows.
In the example we used a fixed offset from UTC to create a new time.Location
reference and named it ourselves. Locations like this always return the same constant timeline.
However, this is an uncommon way to create time.Location
references. In real-world applications they’re almost always loaded from a time zone database.
Time zones
The time.LoadLocation
function allows you to load time.Location
references by a (geographic) name from a time zone database.
The data in the database might be updated, and in turn the timeline provided by time.Location
can change.
This is a thing that happens. Regions occasionally change time zones and everyone will need to update their timezone database.
For example, on December 27th 2020 02:00
Local time the Volgograd region in Russia changed its time zone from UTC+4
to UTC+3
.
This is reflected in the time zone database:
package main
import (
"fmt"
"time"
)
func main() {
volgograd, err := time.LoadLocation("Europe/Volgograd")
if err != nil {
panic(err)
}
// create the UTC times.
utc1 := time.Date(2020, 12, 26, 20, 30, 0, 0, time.UTC)
utc2 := time.Date(2020, 12, 26, 21, 30, 0, 0, time.UTC)
utc3 := time.Date(2020, 12, 26, 22, 30, 0, 0, time.UTC)
// Convert UTC times to the volgograd location.
v1 := utc1.In(volgograd)
v2 := utc2.In(volgograd)
v3 := utc3.In(volgograd)
fmt.Println("UTC offsets:")
fmt.Printf("v1: %s\n", v1.Format("-0700"))
fmt.Printf("v2: %s\n", v2.Format("-0700"))
fmt.Printf("v3: %s\n", v3.Format("-0700"))
}
If the time zone database was an older version that did not reflect this change, all outputs would have returned +0400
as an UTC offset.
We can visualize the volgograd
location providing different timelines as follows:
As you can see, the interval 01:00-01:59
happened twice in volgograd
that day. Once on the UTC+4
timeline and once on the UTC+3
timeline.
To convert a local volgograd
time inside this interval to UTC
, we need to know the appropriate timeline.
Time zone updates are not the only reason a time.Location
provides variable timelines: some geographic regions use daylight saving time.
Daylight saving time
Daylight saving time (DST) is the practice of advancing clocks forward during the warmer months of the year, and setting them back during the colder months.
These clock changes happen at different times of the year in different regions. The rules for DST are also stored in the time zone database.
The Netherlands (well, the European part of The Netherlands) uses daylight saving time according to EU guidelines. Let’s use that as an example.
In the Netherlands, the clock:
- Is advanced on the last Sunday of March at
01:00
UTC, corresponding to02:00
local time. - Is set back on the last Sunday of October at
01:00
UTC, corresponding to03:00
local time.
In 2023, this corresponds to 26th of March
and 29th of October
. We’ll focus on advancing the clock, as setting the clock back is similar to what we saw earlier with the Volgograd time zone change.
package main
import (
"fmt"
"time"
)
func main() {
netherlands, err := time.LoadLocation("Europe/Amsterdam")
if err != nil {
panic(err)
}
// create the UTC times.
utc1 := time.Date(2023, 3, 26, 0, 30, 0, 0, time.UTC)
utc2 := time.Date(2023, 3, 26, 1, 30, 0, 0, time.UTC)
utc3 := time.Date(2023, 3, 26, 2, 30, 0, 0, time.UTC)
// Convert UTC times to the netherlands location.
nl1 := utc1.In(netherlands)
nl2 := utc2.In(netherlands)
nl3 := utc3.In(netherlands)
fmt.Println("UTC offsets:")
fmt.Printf("t1: %s\n", nl1.Format("-0700"))
fmt.Printf("t2: %s\n", nl2.Format("-0700"))
fmt.Printf("t3: %s\n", nl3.Format("-0700"))
}
This example can be visualized as follows:
As you can see, the interval 02:00-02:59
does not exist in netherlands
.
What time.Location to use?
Working with Locations like volgograd
and netherlands
makes things a bit more complicated:
- There can be multiple timelines involved and there are no hard and fast rules when they can change.
- Not every interval in local time is guaranteed to be unique, or to exist.
This makes working with non-UTC times a bit complicated. It’s often recommended to store times as UTC and only convert them to local time when showing them to an end user.
This is a good default, especially for logging, metadata and other “technical timestamps”.
However, it won’t work for all use cases. Sometimes the data you’re working with really is bound to a Location: Shop opening hours, the start time of an event etc.
If you were to store these times as UTC, you’d end up in trouble when the rules for timezones and/or DST change. So be aware of that.
Conclusion
In this article we looked at the two fundamental types in the time
package: time.Time
and time.Location
.
Key takeaways:
- A time instant is a global moment.
- A timeline is required to represent a time instant.
- A
time.Time
value is a representation of a time instant. - Each
time.Time
has a reference to atime.Location
which provides it with a timeline. - Some
time.Location
references use rules to return different timelines. - These rules are sourced from a timezone database.
I hope this article will help you build robust, time-aware applications. Keep these concepts in mind when you work on your next project.
The next article will look at comparing times and those monotonic clocks that I mentioned in the intro.
As always, if you have any questions or comments, feel free to reach out.
Happy coding!
Keep Learning. Subscribe to my Newsletter.
Gain access to more content and get notified of the latest articles:
- A Brief Guide To Time for Developers
- Source code
- Unit tests
I send emails every 1-2 weeks and will keep your data safe. You can unsubscribe at any time.