在日常的代码中,我们经常要和时间间隔(duration)打交道。比如设置超时时间或过期时间、接口调用失败后等几秒再重试、测量某段代码跑了多久,等等。总之——不是在设置等待多久,就是在计算花费了多长时间。
直接使用整数来表示时间间隔也挺好的吧?为什么 Go 非得搞个 time.Duration
出来,是不是有点多此一举?别急,咱们慢慢聊。
使用整数表示时间间隔带来的问题
使用整数表示时间间隔确实简单直接,比如:
- 将超时时间设为
30
; - 为了防止频繁调用 API 或失败后反复重试,每次调用后
sleep(5)
; - 将一段代码的耗时写入日志中,
log.Infof("... elapsed %d", ..., time.Now() - start)
也许读到这里,你也隐约察觉到问题了——光用整数,其实也埋了不少雷:
- 那个超时时间写的
30
,到底是30
秒还是30
毫秒? sleep()
到底要的是秒还是毫秒?我该传个5
,还是5000
?如果是纳秒的话,5
该乘以10
的几次方来着,每次都得查文档- 日志里出现了
... elapsed 10
,这是花费了10
秒——这破代码谁写的,得优化了啊;还是10
毫秒?噢,我的代码性能很好嘛
于是,为了搞清楚到底是秒、毫秒,还是纳秒(其他单位我们也很少用得上),大家只好在“文字游戏”上下苦功夫。
怎么搞呢?要么写文档,然后教育用户:RTFM —— Read The Fuxxing Manual,不看文档出问题别怪我。要么就干脆在变量名、配置项、函数名上加前缀后缀:如 timeout_ms
,usleep()
,UnixNano()
……
造成这一现象的根本原因在于,不管是秒、毫秒还是纳秒,即使单位不同,写出来却都是整数。整数这种基础类型只能表示数值,无法携带单位的信息。
那是不是搞个新类型,让不同单位的时间间隔“戴上身份牌”,就能从根源上避免搞混?Go 拍了拍你肩膀说:我早就准备好了!
Go提供了time.Duration
Go 专门为时间间隔准备了一个类型,叫 time.Duration
。于是我们可以这样设置超时时间:
server := &http.Server{
...
IdleTimeout: 100 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
避免频繁重试时可以这样写:
maxRetry := 5
for i := 0; i < maxRetry; i++ {
...
time.Sleep(5 * time.Second)
}
记录代码耗时时可以这样写:
func DoSomething() {
start := time.Now()
...
log.Infof("DoSomething elapsed %0.3fs", time.Since(start).Seconds())
}
一切看起来都很优雅。一个 *
就让数字有了单位。那是不是 time.Duration
就完美无缺了呢?
time.Duration
也有问题
咱们先来想一个问题——时间间隔能不能参与计算?
比如:
- 你想把几段代码的耗时加起来,算个总和;或者从总耗时里扣掉某一段预热时间;
- 搞个像 TCP 重传那样的策略,每次失败后,下次的等待时间翻倍,以减轻网络拥堵,即下一次
sleep()
前得先把Duration
乘一乘
听起来都是非常合理的需求,对吧?显然,时间间隔是需要支持计算的。
但是,可不是所有的计算都要支持!时间间隔的加减是有意义的,与常数相乘相除也是有意义的,可两个时间间隔相乘就不知道要表示什么了。
func main() {
fmt.Println(1 * time.Second * 1 * time.Second)
// 277777h46m40s
}
在 Go 中,即使你不小心把两个时间间隔相乘,编译器也不会报错。而是会给出一个让人摸不着头脑的结果,比如 1s * 1s
会得出 277777h46m40s
。硬要计算的话,输出 1s^2
,即 1 秒的平方比较合理吧。
为什么会这样呢,这就要从 time.Duration
的实现说起了。
在 Go 里,time.Duration
其实是一个类型别名,本质就是 int64
,单位是纳秒(nanosecond)。而像 time.Second
、time.Millisecond
等其实也只是一些类型为 int64
的常量。
// https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:src/time/time.go
type Duration int64
...
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
所以当执行 1 * time.Second * time.Second
这种操作时,Go 编译器只是把两个 int64
相乘,然后把结果再解释成时间间隔。
其实问题的本质是这样的:time.Duration
是 int64
的别名,所以加减乘除、取余、左移右移、与或非——能对数字做的所有操作,Duration
也照单全收。Go 编译器说了:我只管算,至于有没有物理意义,不归我管。
顺带一提,Rust 就严谨得多。它通过泛型约束和操作符重载,确保 Duration
上的每一次计算都得有意义。
另外,time.Duration
只是个 int64
的别名,这就又带来了个最大值的限制,math.MaxInt64 纳秒 ≈ 290 年
,即 Duration
最多表示大约 290 年之久的时间间隔。
总的来说,虽然 time.Duration
的种种小毛病让人偶尔想吐槽,但它作为时间间隔的表达方式,确实比单纯用整数靠谱得多。不仅能一眼看出单位是秒、毫秒还是纳秒,代码也更加直观易读。要知道,正是因为单位换算问题,1999 年 NASA 的火星气候探测器偏离轨道解体,造成价值 1.25 亿美元的惨重损失。time.Duration
还支持加减、乘除常数等常用操作,让我们在调整或计算时间间隔时更加得心应手。
🔚
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。