跳至正文
首页 » Golang 中 time.Time 不能直接比较

Golang 中 time.Time 不能直接比较

起因

我在开发中写了这样一段代码

type info struct {
  name string
}
data1 := time.Date(2023, 9, 2, 10, 0, 0, 0, time.Local)
timeInfo := map[time.Time]*info{
  data1: {name: "test"},
}
local, _ := time.LoadLocation("Asia/Shanghai")
data2 := time.Date(2023, 9, 2, 10, 0, 0, 0, local)
if _, ok := timeInfo[data2]; !ok {
  fmt.Printf("data1: %v\n", data1)
  fmt.Printf("data2: %v\n", data2)
  fmt.Printf("Equal: %v\n", data1.Equal(data2))
  panic("Not Exist")
}

这段代码只是一个例子,实际上 data1 是使用 gormmysql 中直接解析而来的。结果如下:

data1: 2023-09-02 10:00:00 +0800 CST
data2: 2023-09-02 10:00:00 +0800 CST
Equal: true
panic: Not Exist

我就很纳闷,为啥时间是一样的,但是 map 却没有找到呢。

结论

首先,两个 data 虽然打印的是一样的时间,但是存在结构体中的 Local 是不一样的

data1: Local
data2: Asia/Shanghai
Equal: true
panic: Not Exist

再次,map 判断是否一致,是直接使用的 == 比较的,而 golang 中的 time.Time 是个结构体,结构体比较遵循所有字段比较的原则,所以对于 time.Time 会比较所有字段,包括时区等。

原因

先看 time.Time 的结构体

type Time struct {
  // 简单写一下,详细可以看 time.Time 的源码
 wall uint64
 ext  int64

 loc *Location
}
wall 0 / 000 0000 0000 0000 0000 0000 0000 0000 / 0000 0000 0000 0000 0000 0000 0000 0000
第0位:0 表示没有单调时间;1 表示有单调时间
后33位:表示秒
在后30位:表示纳秒,区间在 [0, 999999999] 刚好在30位内
ext 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

如果wall第0位是0:
1-33 位必须为0,从1年1月1日起有符号的秒级别挂钟时间存在ext中
如果wall第0位是1:
wall 1-33位,从1985年1月1日起无符号的秒级挂钟时间
ext 表示程序启动开始的纳米级别单调时间

为什么需要这么设计

// Now returns the current local time.
func Now() Time {
 sec, nsec, mono := now()
 mono -= startNano
 sec += unixToInternal - minWall
 if uint64(sec)>>33 != 0 {
  // Seconds field overflowed the 33 bits available when
  // storing a monotonic time. This will be true after
  // March 16, 2157.
  return Time{uint64(nsec), sec + minWall, Local}
 }
 return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}

当时间超过 2157年3月16号 之后,33位存储不了,就需要把整个时间放在 ext 中,否则就正常走,详见 time.Timenow 函数。
有个小细节,runtime/now 返回的是一个 int32 只有32位,转换到 uint64 的过程中,最开始的34位一定都是0,因为 nsec[0, 999999999] 刚好在30位内。nsecShift 左移30位,也刚好把后面的 nsec 空出来。

再来看 map 的确定是否存在的函数

func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {
  // 各种检查 ...
 // hash
 hash := t.Hasher(key, uintptr(h.hash0))
// 确定桶 ...
 top := tophash(hash)
bucketloop: // 桶遍历
 for ; b != nil; b = b.overflow(t) {
  for i := uintptr(0); i < bucketCnt; i++ {
   // 桶检查 ...
   if t.Key.Equal(key, k) { // hash 相等
    e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.KeySize)+i*uintptr(t.ValueSize))
    if t.IndirectElem() {
     e = *((*unsafe.Pointer)(e))
    }
    return e, true
   }
  }
 }
 return unsafe.Pointer(&zeroVal[0]), false
}

Equal 对于结构体,会比较结构体中所有字段是否相等

Struct types are comparable if all their field types are comparable.
Two struct values are equal if their corresponding non-blank field values are equal.
The fields are compared in source order, and comparison stops as soon as two field values differ (or all fields have been compared)

所以 time.Time 是不能直接比较的,文档中也有描述

Note that the Go == operator compares not just the time instant but also the Location and the monotonic clock reading.
Therefore, Time values should not be used as map or database keys without first guaranteeing that the identical Location has been set for all values, which can be achieved through use of the UTC or Local method, and that the monotonic clock reading has been stripped by setting t = t.Round(0).
In general, prefer t.Equal(u) to t == u, since t.Equal uses the most accurate comparison available and correctly handles the case when only one of its arguments has a monotonic clock reading.
标签:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注