Go's time.Time and time.Location explained

Avatar of the author Willem Schots
21 Dec, 2023
~8 min.
RSS

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:

A time instant 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.

A diagram showing that the durations between time instants are unknown.

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.
A time instant on different timelines

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.25th 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:

main.go
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.

The example above visualized on two timelines
The time.UTC and eastOfUTC each provide a timeline.

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:

main.go
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:

The Volgograd location returns a different timeline before/after the time zone changes
The moment Europe/Volgograd Location switches from UTC+4 to UTC+3 timeline.

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 to 02:00 local time.
  • Is set back on the last Sunday of October at 01:00 UTC, corresponding to 03: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.

main.go
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:

Advancing the clock in Amsterdam due to DST
The moment the Europe/Amsterdam Location switches from UTC+1 to UTC+2 timeline due to DST.

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 a time.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!

🎓

Subscribe to my Newsletter and Keep Learning.

Gain access to more content and get notified of the latest articles:

I send emails every 1-2 weeks and will keep your data safe. You can unsubscribe at any time.

Hello! I'm the Willem behind willem.dev

I created this website to help new Go developers, I hope it brings you some value! :)

You can follow me on Twitter/X, LinkedIn or Mastodon.

Thanks for reading!