bigywater.tech
Go · 85

time 的单调时钟:Sub 和 == 比较为什么不一样

time.Now() 返回的 Time 里藏着第二个时钟:单调时钟。它让 Sub 和 Since 不受系统时间调整影响,却也让 == 比较、map key 变得出人意料。这篇用实验验证哪些操作保留它、哪些去掉它,以及后果。

m=+0.000114459
time.Now().String() 尾部的单调读数,Round(0) 之后消失
false / true
t == t.Round(0) 与 t.Equal(t.Round(0))
2 个 vs 9 个
实验里保留单调读数的操作(Now、Add)与去掉它的操作

单调读数是什么

实测 · 本机执行输出
  time.Now().String() 的尾部: m=+0.000114459
  同一个值 Round(0) 之后的完整输出:2026-10-10 13:55:29.982922 +0800 CST
  Round(0) 之前的完整输出:2026-10-10 13:55:29.982922 +0800 CST m=+0.000114459

time.Now() 返回的 Time 同时有墙钟读数和单调读数。打印出来尾部的 m=+… 就是后者;文档说墙钟用来报时,单调时钟用来量时。Sub、Since 等在两边都有单调读数时只用它,所以系统时间被校正也不会算出负的耗时。

谁会去掉它

实测 · 本机执行输出
  time.Now():有 m=
  t.Add(time.Hour):有 m=
  t.AddDate(0,0,1):无 m=
  t.Round(time.Second):无 m=
  t.Truncate(time.Second):无 m=
  t.Round(0):无 m=
  t.UTC():无 m=
  t.Local():无 m=
  t.In(time.UTC):无 m=
  time.Unix(t.Unix(), 0):无 m=
  time.Date(…):无 m=

Add 保留,AddDate、Round、Truncate、UTC、Local、In 去掉,Date 和 Unix 构造的值本来就没有。想去掉的标准写法是 t.Round(0)。

比较和 map key

实测 · 本机执行输出
  t == t.Round(0):false;t.Equal(t.Round(0)):true
  经过 JSON 序列化再反序列化:t == u:false;t.Equal(u):true;t.Sub(u) = 0s(JSON 只保留到纳秒的墙钟时间,单调读数丢了)
  用 time.Time 当 map 的 key:用 t 查 true;用 t.Round(0) 查 false(同一个时刻!)
  同一时刻、不同时区:d == l:false;d.Equal(l):true

== 比较的不只是时刻,还有时区和单调读数:t == t.Round(0) 是 false,而 Equal 是 true;用 time.Time 当 map 的 key,用同一时刻去掉单调读数的值就查不到。JSON 往返之后也是 == 不等、Equal 相等。比较时刻永远用 Equal,不要用 ==;做 map key 前先 Round(0) 或者转成 Unix 时间。

成本

实测 · 本机执行输出
  time.Now() 32 ns/次;time.Since(t) 15 ns/次;t.Round(0) 2 ns/次

time.Now() 约 32 ns,time.Since 约 15 ns,Round(0) 约 2 ns。对绝大多数程序不是问题。

我没法验证的部分

实测 · 本机执行输出
  The general rule is that the wall clock is for telling time and the monotonic clock is for measuring time.
  The canonical way to strip a monotonic clock reading is to use t = t.Round(0).
  If Times t and u both contain monotonic clock readings, the operations t.After(u), t.Before(u), t.Equal(u), t.Compare(u), and t.Sub(u) are carried out using the monotonic clock readings alone, ignoring the wall clock readings. If either t or u contains no monotonic clock reading, these operations fall back to using the wall clock readings.
  On some systems the monotonic clock will stop if the computer goes to sleep.
  Note that the Go == operator compares not just the time instant but also the Location and the monotonic clock reading. See the documentation for the Time type for a discussion of equality testing for Time values.  For debugging, the result of t.String does include the monotonic clock reading if present. If t != u because of different monotonic clock readings, that difference will be visible when printing t.String() and u.String().

要证明“系统时间被调整时 Sub 仍然正确”,需要真的修改系统时钟,这需要管理员权限,我没有做,所以这一条只引用文档。休眠时单调时钟停止的说法同样来自文档。

分步演示

time 的单调时钟
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。所有输出来自单独的小程序;成本用 testing.Benchmark;文档引自 go doc time 的 Monotonic Clocks 一节(逐句核对过原文存在)
    • 没有验证:系统时钟被调整时 Sub/Since 的行为(需要管理员权限修改时钟)、休眠对单调时钟的影响、Linux 与 macOS 的差别、time.Time 在 gob/protobuf 等序列化中的行为
    京ICP备2026067502号-1京公网安备11010502063487号