大家好,我是煎鱼
在 Go1.23 以前,标准库 time
除了 After
方法外。还有另外一个问题,那就是 Stop
和 Reset
方法不太靠谱。
以至于大家在一些特殊场景下总是这踩点坑,那踩点坑。无论如何,解决了就值得我们给 rsc 鼓掌!
问题背景
最早的反馈来自 2016 年的 time: document proper usage of Timer.Stop[1],随后在 2020 年终于有人正式提出且下面这个问题[2]:
问题的代码示例:
func main() {
timeout := 50 * time.Millisecond
t := time.NewTimer(timeout)
time.Sleep(100 * time.Millisecond)
start := time.Now()
t.Reset(timeout)
<-t.C
fmt.Printf("煎鱼已经消失:%dms\n", time.Since(start).Milliseconds())
}
先暂停,思考一下。
这段程序输出的结果是什么?是 “煎鱼已经消失:100ms” 吗,还是 50ms?
Go1.22 及以前的版本下,输出结果如下:
煎鱼已经消失:0ms
结果是:“煎鱼已经消失:0ms”。是不是有些与想象中不一样。
原因分析
程序走读
在 Go1.22 及以前的版本上,Go 官方文档在 Reset 方法说明上,明确要求:对于使用 NewTimer 创建的计时器,只有在通道(channel)耗尽的定时器停止或过期时才会调用重置。
结合程序来看,计时器 t 的超时时间为 50 毫秒。在 Sleep 方法等待 100 毫秒后,计时器 t 早已经过期,向 t.C 通道发送了一个值。
但由于 Reset 方法并不会耗尽通道,因此 <-t.C 不会阻塞,并立即继续程序。
(人家压根没阻塞,程序想必就直接输出了 “煎鱼已经消失:0ms”。
根因说明
在 Go1.23 之前,与计时器相关联的通道是异步的(缓冲,容量为 1),这意味着即使在 Timer.Stop
或 Timer.Reset
返回后,也能接收到过期的时间值。(导致程序与预期不符的根本原因)
从 Go1.23 开始,该通道是同步通道(无缓冲,容量为 0),从而消除了出现过期时间值的可能性。
Go1.23 终于正常了!
我们回到新发布的 Go1.23 RC 版本,该问题终于被修复,皆大欢喜!
当我们再次运行上述程序后,输出结果:
煎鱼已经消失:50ms
程序运行结果符合预期。
总结
历经了 8 年左右,从在 Go1.7 左右发现到愿意承认这个问题,再到再 Go1.23 真正去修复 Timer,感觉也是非常不容易的。
本次在 Go1.23 起,Time 的两个坑 After 和 Reset 都得到了解决,后面大家也可以省心不少了。唯一遗憾的就是明知有问题,解决的还是比较久了(可能优先级比较低?)。
推荐阅读
参考资料
[1]
time: document proper usage of Timer.Stop: https://github.com/golang/go/issues/14383
[2]问题: https://github.com/golang/go/issues/37196
关注和加煎鱼微信,
一手消息和知识,拉你进技术交流群👇
你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路。
日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!
原创不易 点赞支持