在日常的代码中,我们经常要和时间间隔(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_msusleep()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.Secondtime.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.Durationint64 的别名,所以加减乘除、取余、左移右移、与或非——能对数字做的所有操作,Duration 也照单全收。Go 编译器说了:我只管算,至于有没有物理意义,不归我管。

顺带一提,Rust 就严谨得多。它通过泛型约束和操作符重载,确保 Duration 上的每一次计算都得有意义。

另外,time.Duration 只是个 int64 的别名,这就又带来了个最大值的限制,math.MaxInt64 纳秒 ≈ 290 年 ,即 Duration 最多表示大约 290 年之久的时间间隔。


总的来说,虽然 time.Duration 的种种小毛病让人偶尔想吐槽,但它作为时间间隔的表达方式,确实比单纯用整数靠谱得多。不仅能一眼看出单位是秒、毫秒还是纳秒,代码也更加直观易读。要知道,正是因为单位换算问题,1999 年 NASA 的火星气候探测器偏离轨道解体,造成价值 1.25 亿美元的惨重损失。time.Duration 还支持加减、乘除常数等常用操作,让我们在调整或计算时间间隔时更加得心应手。

🔚


da_miao_zi
1 声望0 粉丝

软件工程师、技术图书译者。译有《图解云计算架构》《图解量子计算机》《计算机是怎样跑起来的》《自制搜索引擎》等。