go-中-time.Since-失效,手动修改系统时间后不生效time-包的单调时钟设计
目录
go 中 time.Since 失效,手动修改系统时间后不生效?(time 包的单调时钟设计)
前言
最近在做一个时间相关的任务,判断当前时间和记录之间差值大于某个时间时,则触发操作。但是测试人员在测试过程中却报了 bug
,修改了系统时间后,预想的操作没有触发,挺简单的一个功能啊,为啥呢?
go
中使用了单调时钟(monotonic clock)设计,用于精确测量时间间隔,避免系统时间被修改导致计算错误。所以本地时间无论怎么修改,time.Since
得到的都是时间差。
问题回溯
又过了一遍代码,看了逻辑没啥问题,写个小测试看下
package main
import (
"fmt"
"time"
)
func main() {
// 先获取一个时间
t0 := time.Now()
fmt.Println("当前时间:", t0)
delay := time.Hour
ticker := time.NewTicker(time.Second)
for range ticker.C {
if time.Since(t0) > delay {
fmt.Println("time.Since(t0) > delay")
break
}
if (time.Now().Unix() - t0.Unix()) > int64(delay.Seconds()) {
fmt.Println("(time.Now().Unix() - t0.Unix()) > delay.Seconds()")
break
}
}
}
测试过程中需手动修改系统时间,从程序启动时间向后调整 2
小时,预想的通过 time.Since
跳出循环,但是并没有,最终是通过 time.Now().Unix() - t0.Unix()
跳出循环的,time
包提供的方法都不起作用,为什么会这样?
源码分析
使用 go
版本 1.25.1
,发现 go
的 time
包使用了单调时钟设计。
time.Since
的实现:
// time/time.go
func Since(t Time) Duration {
// 如果原始时间包含单调时钟信息
if t.wall&hasMonotonic != 0 && !runtimeIsBubbled() {
return subMono(runtimeNano()-startNano, t.ext)
}
return Now().Sub(t)
}
关键点在于 t.wall&hasMonotonic != 0
这个判断。当使用 time.Now()
获取时间时,go
会同时记录系统时间和单调时钟时间。
// time/time.go
func Now() Time {
sec, nsec, mono := runtimeNow()
if mono == 0 {
return Time{uint64(nsec), sec + unixToInternal, Local}
}
mono -= startNano
sec += unixToInternal - minWall
if uint64(sec)>>33 != 0 {
return Time{uint64(nsec), sec + minWall, Local}
}
return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}
当系统时间被修改后,time.Now()
返回的新时间仍然包含单调时钟信息,但单调时钟是基于系统启动时间的,不会因为系统时间修改而改变。
time.Now()
├─ walltime() ── CLOCK_REALTIME ──> 给人看的时间戳
└─ nanotime() ── CLOCK_MONOTONIC ──> 给程序算间隔
↓
打包成 1 个 Time 结构体(wall+ext)
↓
业务代码 Sub/Add/After/Before
├─ 两边都有 mono ➜ 用 mono,绝对可靠
└─ 任一边缺 mono ➜ 用 wall,保证语义一致
单调时钟
- 单调时钟是一个从某个固定点(如系统启动)开始递增的时钟,不会被系统时间修改影响。
- 它只用于计算时间差,不用于显示时间。
- 这种设计是为了避免系统时间调整(如NTP同步、手动修改等)对时间计算造成的影响。
解决方案
上述分析已经提供了一种方案,只要把这个单调时钟时间去掉就行了。
func t1() {
delay := time.Hour
// 直接将 t0 构建为不带单调时钟的时间
t0 := time.Unix(time.Now().Unix(), 0)
fmt.Println("当前时间:", t0)
ticker := time.NewTicker(time.Second)
for range ticker.C {
if time.Since(t0) > delay {
fmt.Println("time.Since(t0.Round(0)) > delay")
break
}
}
}
func t2() {
delay := time.Hour
t0 := time.Now()
fmt.Println("当前时间:", t0)
ticker := time.NewTicker(time.Second)
for range ticker.C {
if time.Since(t0.Truncate(0)) > delay {
fmt.Println("time.Since(t0.Truncate(0)) > delay")
break
}
if time.Since(t0.Round(0)) > delay {
fmt.Println("time.Since(t0.Round(0)) > delay")
break
}
if (time.Now().Unix() - t0.Unix()) > int64(delay.Seconds()) {
fmt.Println("(time.Now().Unix() - t0.Unix()) > delay.Seconds()")
break
}
}
}
总结
Go 的 time.Since
使用单调时钟设计,这是为了提供稳定可靠的时间差计算。当系统时间被修改时,单调时钟不会受到影响,这是 go
时间包的设计特性,而不是 bug
。