Jason Pan

Go 墙上时间和递增时间

潘忠显 / 2024-07-09


在计算机系统中,有两个常见的时间:

在多数情况下,墙上时间用于记录事件的实际发生时间,例如文件的创建时间、日志的时间戳等。而递增时间用来计算时间精确的时间间隔,不能受到系统时间调整干扰。


在 Linux 系统上,我们可以通过 man clock_gettime 可以看到系统调用 clock_gettime() 可以通过接收不同的参数,获得不同的时间,使用 CLOCK_REALTIME 获得墙上时间,而使用 CLOCK_MONOTONIC 获得递增时间

Go 语言中,我们要如何获得我们想要的时间呢?

标准库 time 文档中,有提到说 time.Now 同时包含了这两个时间:

Rather than split the API, in this package the Time returned by time.Now contains both a wall clock reading and a monotonic clock reading; later time-telling operations use the wall clock reading, but later time-measuring operations, specifically comparisons and subtractions, use the monotonic clock reading.

time.Time 结构体

我们看一下 Time 结构体:

为了更清楚地展示,我这里画出了 hasMonotonic 分别为 0 和 1 的分布:

time-struct-with-monotonic

time-struct-without-monotonic

time-struct-code

【注意】上边提到的、以及注释中的 hasMonotonic 指的是 wall 中的一个 bit,要跟 time.hasMonotonic 这个常量区分开,后者是用作取 wall 中 bit 的掩码。所以你到处会看到 t.wall&hasMonotonic != 0 类似的判断。

time.Now() 函数

接下来再看一下 Now() 函数,他最终返回一个 Time 结构,可以看到这里会调用内部的 now() 函数,获得墙上时间的秒、纳秒,以及递增时间 mono,最后会将这三个值构建一个 Time 结构对象。

注意这里的细节,wall 拼接跟前面我们描述的规则一致:将标志位置为 1,填充两个时间戳,并且使用 ext 存储 mono

time-struct-with-monotonic-1

time-now-code

time.Unix(sec, nsec) 函数

我们也比较常用将时间戳转换成时间,会用到函数 time.Unix(sec, nsec),也会返回一个 Time 对象。

但我们通过源码可以看到,他跟上边提到的 time.Now() 不同,他不会使用 hasMonotonic,并且直接将时间戳的秒填充到 ext:

time-struct-without-monotonic-1 time-unix-code

time.Time.Unix() 函数

我们自己写代码,经常用诸如 int(time.Now().Unix()) 的代码来获得当前时间戳。

这里 Unix() 函数代码如下,使用的就是墙上时间。

【注意】这里出现了 t.wall&hasMonotonic != 0 的判断,根据不同的场景,从不同的位置获取时间戳值。

time-time-unix-code

time.Time.Add() 函数

而我们也经常用 Add() 函数,比如在当前时间增加几秒,以控制超时或者其他操作。

而这个 Add() 函数本质会调用到 addSec(),后者可以判断原来的 Time 到底是有单调时间的,还是没有单调时间:

time-time-add-code

时间比较函数

时间比较函数不涉及到加减超过表达范围,会直接依据原来的类型进行比较。

只有当比较的两个时间都具有单调时间的时候,使用单调时间比较,否则都使用墙上时间比较:

time-time-comp-code

小结

通过阅读 time 包的源码,我们搞清楚了 Go 是如何通过一个 Time 结构来同时维护着墙上时间单调时间两个时间的。

这里将以上转换关系汇总成一个图,方便大家理解。

time-state-in-go