time
作为使用频次非常高的包, Go Team 是如何实现这个包的呢?里面有多少可以挖掘的小技巧呢?没错,由于自 1.9 后,Go Team 更新了 time
包的实现,其中主要变更部分更是由 Russ Cox 操刀,那么我们今天就来源码解析下它。
注意:本文的源码解析基于 1.10.2 版本。
Time
我们都知道 time
中有三个比较常用的数据结构,分别为:
- time.Time
- time.Duration
- time.C
而下面我们来逐一解析它们的作用。
最常见的情况下,我们会通过 time.Now()
函数得到一个 time.Time
类型的变量,这个变量代表着当前的时间,但是有多少人知道 time.Time
类型里面的字段都代表着什么含义呢?
在讲解 time.Time
结构之前,我们需要知道两个概念:
- wall time
- monotonic time
如果你已经知道这两个时间的概念,那么可以跳过这一段。
wall time 顾名思义,就是挂在墙上的时钟,我们在计算机中能看到的当前时间就是 wall time ,但是这个时间是可以通过 人为设置 或者 NTP服务同步 被修改,常见的场景就是通过修改时间延长收费软件的试用期。
monotonic time 是一个单调递增的时间,当操作系统被初始化时, jiffies 变量被初始化为 0 ,每当接收到一个 timer interrupt ,则 jiffies 自增 1 ,所以它必然是一个不可修改的单调递增时间。
所以,在操作系统中如果需要 显示 时间的时候,会使用 wall time ,而需要 测量 时间的时候,会使用 monotonic time 。
但是为了避免拆分 API, time
包将这两个时间合并在 time.Time
这个结构中,当需要读取时间的时候会使用 wall time ,如果需要测量时间就会使用 monotonic time。
下面简单举例使用两种不同时间的函数,但需要注意的时候,如果两个时间同时存在 monotonic time ,则会忽略 wall time ,反之才会使用 wall time:
- wall time
- time.Since(start)
- time.Until(deadline)
- time.Now().Before(deadline)
- monotonic time
- t.AddDate(y, m, d)
- t.Round(d)
- t.Truncate(d)
多说无益,我们来看下 time.Time
是怎么同时存储 wall time 以及 monotonic time 的。
// src/time/time.go
// 1. Time 能够代表纳秒精度的时间
// 2. 因为 Time 并非并发安全,所以在存储或者是传递的时候,都应该使用值引用。
// 3. 在 Go 中, == 运算符不仅仅会比较时刻,还会比较 Location 以及单调时钟,
// 因此在不保证所有时间设置为相同的位置的时候,不应将 time.Time
// 作为 map 或者 database 的键。如果必须要使用,应先通过 UTC 或者 Local
// 方法将单调时钟剥离。
//
type Time struct {
// wall 和 ext 字段共同组成 wall time 秒级、纳秒级,monotonic time 纳秒级
// 的时间精度,先看下 wall 这个 无符号64位整数 的结构。
//
// +------------------+--------------+--------------------+
// wall => | 1 (hasMonotonic) | 33 (second) | 30 (nanosecond) |
// +------------------+--------------+--------------------+
//
// 所以 wall 字段会有两种情况,分别为
// 1. 当 wall 字段的 hasMonotonic 为 0 时,second 位也全部为 0,ext 字段会存储
// 从 1-1-1 开始的秒级精度时间作为 wall time 。
// 2. 当 wall 字段的 hasMonotonic 为 1 时,second 位会存储从 1885-1-1 开始的秒
// 级精度时间作为 wall time,并且 ext 字段会存储从操作系统启动后的纳秒级精度时间
// 作为 monotonic time 。
wall uint64
ext int64
// Location 作为当前时间的时区,可用于确定时间是否处在正确的位置上。
// 当 loc 为 nil 时,则表示为 UTC 时间。
// 因为北京时区为东八区,比 UTC 时间要领先 8 个小时,
// 所以我们获取到的时间默认会记为 +0800
loc *Location
}
为了加深理解 time.Time
的存储机制,我们通过以下的方法进行解析。
// src/time/time.go
const (
// 代表无符号64位整数的首位
hasMonotonic = 1 << 63
// maxWall 和 minWall 是指 hasMonotonic 为 1 的情况下,
// wall time 的最大以及最小的时间范围。
maxWall = wallToInternal + (1<<33 - 1) // year 2157
minWall = wallToInternal // year 1885
// 纳秒位位置的辅助常量
nsecMask = 1<<30 - 1
nsecShift = 30
)
// 注意:以下方法均为包内辅助方法,会通过指针接收器进行操作,减轻调用负担,
// 但我们在使用 time.Time 时应该尽量避免使用指针,以免出现竞态争用。
// sec 返回时间的秒数
func (t *Time) sec() int64 {
if t.wall&hasMonotonic != 0 {
// hasMonotonic 为 1,则 second 位记录从 1885-1-1 开始的秒数
// 则返回值为以下两个值相加:
// wallToInternal = 1885-1-1 00:00:00 的秒数 = 59453308800
// t.wall<<1>>(nsecShift+1) = 1885-1-1 00:00:00 到现在的秒数
return wallToInternal + int(t.wall<<1>>(nsecShift+1))
}
return int64(t.ext) // hasMonotonic 为 0 ,返回 ext 为 wall time 秒数
}
// addSec 在当前时间基础上加上 d 秒
func (t *Time) addSec(d int64) {
if t.wall&hasMonotonic != 0 {
// 同上,获取当前秒数
sec := int64(t.wall << 1 >> (nsecShift + 1))
dsec := sec + d
if 0 <= dsec && dsec <= 1<<33-1 { // 判断 wall 的 second 不会溢出
t.wall = t.wall&nsecMask | uint64(dsec)<<nsecShift | hasMonotonic
return
}
// second 位已经不足以存下了 wall time 的秒数,需要去掉单调时钟,并
// 其移动到 ext 字段中,移动完成后,执行下面的 t.ext += d 语句即可
t.stripMono()
}
// 如果 hasMonotonic 为 0,直接就在 ext 字段上面添加就好了
t.ext += d
}
// stripMono 去除单调时钟
func (t *Time) stripMono() {
if t.wall&hasMonotonic != 0 {
t.ext = t.sec()
t.wall &= nsecMask
}
}
以上的这些方法都是 time
包中的匿名方法,接下来我们看看平常能使用到的 After
/ Before
/ Equal
等方法。
// src/time/time.go
// 判断 t 时间是否晚于 u 时间
func (t Time) After(u Time) bool {
if t.wall&u.wall&hasMonotonic != 0 { // 判断 t 和 u 是否都有单调时钟
return t.ext > u.ext // 只需判断单调时钟即可
}
ts := t.sec() // 否则需要从 wall 字段中获取秒数
us := u.sec()
// 判断 t 的秒数是否大于 u
// 如果秒数相同,则比较纳秒数
return ts > us || ts == us && t.nsec() > u.nsec()
}
// 判断 t 时间是否早于 u 时间
func (t Time) Before(u Time) bool {
if t.wall&u.wall&hasMonotonic != 0 { // 同上
return t.ext < u.ext
}
// 同上反之
return t.sec() < u.sec() || t.sec() == u.sec() && t.nsec() < u.nsec()
}
// 判断 t 与 u 时间是否相同,不判断 location
// 如 6:00 +0200 CEST 与 4:00 UTC 会返回 true
// 如需同时判断 location ,可以使用 == 操作符
func (t Time) Equal(u Time) bool {
if t.wall&u.wall&hasMonotonic != 0 { // 同上
return t.ext == u.ext
}
// 判断 t 与 u 的秒数以及纳秒数是否相同
return t.sec() == u.sec() && t.nsec() == u.nsec()
}
这里其实可以将 Before
函数实现为: return !t.After(u)
是否会更清晰些?而且从代码实现细节上看 Before
与 After
上有细微的差别,从 git blame 看也是同一人所写,不清楚是为什么,如有知道此处细节的同学可以留言解释下,谢谢。
其实从上面 3 个函数可以看出来,如果两个时间都是带有单调时钟的时候,处理时候会更高效,那么我们怎么才能将获取带有单调函数的时间呢?
Now
其实我们从 time
包中获取时间最常用的函数就是 time.Now()
了,该函数返回机器的当前时间,我们来看看具体是如何实现的?
// src/time/time.go
const (
// unix 时间戳为从 1970-01-01 00:00:00 开始到当前的秒数
// time 包的 internal 时间为从 0000-00-00 00:00:00 开始的秒数
//
// 以下两个常量用于在 internal 与 unix 时间戳之间转换的辅助常量
unixToInternal int64 = (1969*365 + 1969/4 - 1969/100 + 1969/400) * secondsPerDay
internalToUnix int64 = -unixToInternal
// minWall = wallToInternal
wallToInternal int64 = (1884*365 + 1884/4 - 1884/100 + 1884/400) * secondsPerDay
internalToWall int64 = -wallToInternal
)
// Provided by package runtime.
func now() (sec int64, nsec int32, mono uint64)
// darwin,amd64 darwin,386 windows => src/runtime/timeasm.go
// other => src/runtime/timestub.go
// 返回当前本机时间
func Now() Time {
sec, nsec, mono := now()
// 计算从 1885-1-1 开始到现在的秒数
// unixToInternal = 1970-01-01 00:00:00
// minWall = 1885-01-01 00:00:00
sec += unixToInternal - minWall
if uint64(sec)>>33 != 0 { // 如果有溢出,则不能用 wall 的 second 保存完整的时间戳
// 返回自 1970-01-01 00:00:00 开始的秒数
return Time{uint64(nsec), sec + minWall, Local}
}
return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}
我们可以看到,Now
函数主要依赖的是私有函数 now
返回的三个变量 second
/ nsec
/ mono
,而 now
函数的实现则交由 runtime
实现,因为其实现为汇编,不在本文探讨范围内,读者只需要知道该函数会返回本机的当前 秒 、纳秒 以及 单调时钟 即可,如有兴趣的读者可以留言,我会另写一文解析 now
的汇编实现。
此外,我们还可以通过如下函数获取 time.Time
,但因为它们具体实现基本一致,所以我们只挑其中一个解析。
- time.Unix(sec, nsec)
- time.Date(year, month, day, hour, min, sec, nsec, loc)
- time.ParseInLocation(layout, value, loc)
time.Unix(sec, nsec)
函数通过传入 unix timestamp 获取 time.Time
结构,默认返回的是 UTC 时区。
// src/time/time.go
// 通过传入自 1970-01-01 00:00:00 开始的参数生成对应时间点的变量
func Unix(sec int64, nsec int64) Time {
// 如果 nsec 不处于 [0, 999999999] 的闭区间
if nsec < 0 || nsec > 1e9 {
// 将超出 nsec 正常精度的数值转移到 sec 中
n := nsec / 1e9
sec += n
nsec -= n * 1e9
if nsec < 0 {
nsec += 1e9
sec--
}
}
return unixTime(sec, int32(nsec))
}
func unixTime(sec int64, nsec int32) Time {
// 模拟没有单调时钟的时间结构
// wall => uint64(nsec)
// ext => sec+unixToInternal
return Time{uint64(nsec), sec+unixToInternal, Local}
}
通过以上解析的函数实现可知,如果想获取带有 单调时钟 的时间只能通过 time.Now()
获取,而由于 wall 的 second 有 33 位,所以只要我们在 2157-01-01 00:00:00 UTC 前调用 time.Now()
获取到的时间都是带有 单调时钟 的,所以放心使用吧~