起因
我在开发中写了这样一段代码
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 是使用 gorm 从 mysql 中直接解析而来的。结果如下:
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.Time 中 now 函数。
有个小细节,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.