It’s tempting to think of time (and dates by extension) as an ever increasing number. While you’re developing in Go you might even try to compare two time.Time values using the comparison operators.
You will then find out that comparisons using >, <, <= or >= won’t compile:
invalid operation: t > u (operator > not defined on struct)
Comparisons using == and != will compile, but you should be careful, it probably doesn’t do what you intended to do!
So what is going on? time.Time values don’t seem to be like integers or floats.
What is time.Time
By checking the docs, we can see that the time.Time type is a struct:
type Time struct {
// contains filtered or unexported fields
}
Structs are allowed to be compared using == and !=, but not with other operators. Which explains why comparison operators like > and <= don’t compile.
Why is time.Time a struct?
Time on computers is more complex than you might initially expect:
- Telling time involves clocks that are shaped by society. These clocks can essentially be “turned back” at any moment. They are not always suitable for measuring or comparing time. Read more about representing/telling time.
- Measuring time is best done by clocks that a guaranteed to only move forward. Most computers contain such a monotonic clock. Learn more about
time.Now()and monotonic clocks.
An integer or float would not be enough to store all the necessary data. This is why time.Time is a struct.
It contains:
- The location reference. A pointer to a
time.Location. - Two numbers encoding the wall element for telling time and the monotonic element for measuring time.
The monotonic element is only set when a time.Time is created via time.Now(), or when such a value is provided to t.Add(d). The system on which this happens also needs to have monotonic clock (most do).
Comparing time instants
What you are usually interested in, is not comparing the time value itself, but the time instant (moment in time) it represents. Different clocks can represent the same time instant with different readings.
In the time.Time struct there are potentially two kinds of representations:
- The wall element used together with a
time.Locationreference. - The monotonic element.
When comparing time.Time values they need to be handled differently:
- Wall elements can represent the same time instant but be in different locations.
- Monotonic elements can only be compared with other monotonic elements, but not every
time.Timevalue has one.
That’s why all the comparison methods use the following logic:
If both time.Time values have a monotonic element, use that for the comparison. Otherwise, use the wall element.
When using the wall element, location is always kept into account as you will see in the following examples.
Equal
The t.Equal(u) method checks if the time instants represented by t and u are the same.
package main
import (
"fmt"
"time"
)
func main() {
// first compare using wall element
eastOfUTC := time.FixedZone("UTC+1", 60*60)
t := time.Date(2024, 1, 18, 13, 37, 0, 0, time.UTC)
u := time.Date(2024, 1, 18, 14, 37, 0, 0, eastOfUTC)
fmt.Printf("t.Equal(u): %v\n", t.Equal(u))
// then compare using monotonic element.
m := time.Now()
k := m // copy m
fmt.Printf("m.Equal(k): %v\n", m.Equal(k))
}
In the example above tand u have different wall clock elements of 13:37 and 14:37. They are still equal because they represent the same time instant.
t and u represent the same time instant on different timelines and are equal.Likewise, m and k are equal, but they are compared using their monotonic elements:
m and k represent the same time instant on the monotonic timeline and are equal.Before and After
The t.Before(u) method checks if the time instant represented by t is before the time instant represented by u.
Similarly, the t.After(u) method checks if the time instant represented by t comes after the time instant represented by u.
package main
import (
"fmt"
"time"
)
func main() {
// first compare using wall element
eastOfUTC := time.FixedZone("UTC+1", 60*60)
t := time.Date(2024, 1, 18, 13, 30, 0, 0, time.UTC)
u := time.Date(2024, 1, 18, 13, 45, 0, 0, eastOfUTC)
fmt.Printf("t.After(u): %v\n", t.After(u))
fmt.Printf("u.Before(t): %v\n", u.Before(t))
// then compare using monotonic element.
m := time.Now()
time.Sleep(2 * time.Nanosecond)
k := time.Now()
fmt.Printf("m.Before(k): %v\n", m.Before(k))
fmt.Printf("k.After(m): %v\n", k.After(m))
}
As you can see, this does not check the wall element (13:30 vs 13:45) of u and t. It actually checks if represented time instant comes first.
u's time instant comes before t's time instant because they use different timelines.m and k are again using the monotonic elements for this comparison.
m comes before k on the monotonic timeline.Compare
The t.Compare(u) method combines the Equal, Before and After methods. It returns:
-1: iftis beforeu.0: iftis the same asu.+1: iftis afteru.
package main
import (
"fmt"
"time"
)
func main() {
// first compare using wall element
eastOfUTC := time.FixedZone("UTC+1", 60*60)
t := time.Date(2024, 1, 18, 13, 30, 0, 0, time.UTC)
u := time.Date(2024, 1, 18, 13, 45, 0, 0, eastOfUTC)
fmt.Printf("t.Compare(t): %v\n", t.Compare(t))
fmt.Printf("t.Compare(u): %v\n", t.Compare(u))
fmt.Printf("u.Compare(t): %v\n", u.Compare(t))
// then compare using monotonic element.
m := time.Now()
time.Sleep(2 * time.Nanosecond)
k := time.Now()
fmt.Printf("m.Compare(m): %v\n", m.Compare(m))
fmt.Printf("m.Compare(k): %v\n", m.Compare(k))
fmt.Printf("k.Compare(m): %v\n", k.Compare(m))
}
IsZero
The t.IsZero() method returns true when the time instant represented by t is the same one that is represented by "January 1, year 1, 00:00:00 UTC".
IsZero does not check that the fields of the struct are all zero.
The IsZero method never uses the monotonic element.
package main
import (
"fmt"
"time"
)
func main() {
// first compare using wall element
eastOfUTC := time.FixedZone("UTC+1", 60*60)
t := time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)
u := time.Date(1, 1, 1, 1, 0, 0, 0, eastOfUTC)
fmt.Printf("t.IsZero(): %v\n", t.IsZero())
fmt.Printf("u.IsZero(): %v\n", u.IsZero())
}
t and u both represent "January 1, year 1, 00:00:00 UTC" on different timelines.Comparison methods
We just saw how to compare time.Time values using the available comparison methods.
Because time.Time is a struct, it’s also possible to check for (non) equality using the == and != operators.
The only time (that I can think of), where you might consider this is when using the time.Time type as a map key.
How to make == and != work
Just to be clear, it’s safer and easier to use the t.Equal(u) method.
There are linters that can warn you when you're using == instead of Equal this.
The == and != operators will compare time.Time on a field-by-field basis. Due to the way the data is encoded into the struct, comparisons will only give valid results under certain conditions.
For values to be equal using ==:
- Both values need values need to hold the same
time.Locationpointer. - Both values need to have the same wall element.
- Either both are missing a monotonic element, or both have the exact same monotonic element.
The most straightforward way to achieve this is by:
- Ensuring they are in the same location using
t.UTC(),t.Local()ort.In(loc *time.Location). - Ensuring the monotonic elements are stripped using
t.Round(0).
For an example, see the next section.
time.Time as map key
When times are “normalized” this way, we can compare them using == and use them as map keys:
package main
import (
"fmt"
"time"
)
func main() {
m := make(map[time.Time]string, 0)
now := time.Now()
m[now] = "stays the same"
normNow := now.UTC().Round(0)
m[normNow] = "is overwritten"
hardcodedNow := time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC)
m[hardcodedNow] = "a new value"
fmt.Println(m)
}
The map in the above example will contain two elements. One with the value "stays the same" and one with "a new value".
The now time is not normalized, and will not equal any of the later times because it contains a monotonic element.
In the playground environment the time will always begin at "2009-11-10 23:00:00 UTC" which makes normNow equal the hardcodedNow.
Summary
In this article we discussed how to compare time.Time in different ways.
The key points are:
time.Timecontains two time representations: wall element and monotonic element.- The monotonic element is used when all compared values have it.
- Otherwise, the comparison methods fall back to using wall time.
- When comparing using wall time, the location is taken into account.
- Under specific conditions you can compare
time.Timevalues using==or!=.
I hope you learned something. Feel free to reach out if you have comments or questions.
Happy coding!
Get my free newsletter periodically*
Used by 500+ developers to boost their Go skills.
*working on a big project as of 2025, will get back to posting on the regular schedule once time allows.