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.
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.Location
reference. - 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.Time
value 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 t
and u
have different wall clock elements of 13:37
and 14:37
. They are still equal because they represent the same time instant.
Likewise, m
and k
are equal, but they are compared using their monotonic elements:
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.
m
and k
are again using the monotonic elements for this comparison.
Compare
The t.Compare(u)
method combines the Equal
, Before
and After
methods. It returns:
-1
: ift
is beforeu
.0
: ift
is the same asu
.+1
: ift
is 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())
}
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.
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.Location
pointer. - 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.Time
contains 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.Time
values using==
or!=
.
I hope you learned something. Feel free to reach out if you have comments or questions.
Happy coding!
Get my free newsletter every second week
Used by 500+ developers to boost their Go skills.
"I'll share tips, interesting links and new content. You'll also get a brief guide to time for developers for free."