前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >go time 包关于时钟的理解

go time 包关于时钟的理解

原创
作者头像
seif
发布2022-09-29 15:41:06
4470
发布2022-09-29 15:41:06
举报
文章被收录于专栏:技术干货推荐

| 导语 最近看书再次看到了墙上时钟与单调时钟,瞬间勾起了对 go time 关于这两种时钟的支持与使用。以下内容都来自对官方文档的解读与理解。

引言--时钟的重要性

在现代计算机领域中,时钟几乎无处不在,特别是当今分布式系统流行的今天许多场景更是依赖于时钟,例如:

  • 请求是否超时
  • 某项服务 p99 响应时间为多少
  • 某个服务的 qps 是多少
  • 缓存何时过期
  • 分布式节点失联多长时间将其剔除
  • 用户在浏览短视频时在某个视频停留时间
  • 用户查看广告的时间

可以这么说,离开了时钟,几乎所有的服务都将无法正常工作。但是时钟却不是我们想象的那么可靠,比如几台机器使用 NTP 来同步时间,那么很可能一台机器的墙上时间走着走着忽然回退了。这些微妙的小问题,往往会引发一些未知的错误。接下来带你去看看 go time 包关于时钟的处理,首先来了解下墙上时钟与单调时钟。

墙上时钟(wall clock) 与单调时钟(monotonic clock)

墙上时钟

墙上时钟根据墙上时间返回当前的日期与事件。一般会返回自纪元1970年1月1日(UTC)以来的秒数和毫秒数,不含闰秒。但不是绝对的,有些系统会使用其他日期作为参考点(可参考:https://zh.wikipedia.org/wiki/系统时间)。

墙上时钟一般是要与 NTP 同步的,但是如果本地时钟要是远远快于 NTP 服务器,就会强行重置,导致出现上文说得时间会回退。所以墙上时钟不适合测量时间间隔。

单调时钟

单调时钟顾名思义是总是向前的。所以它适合用来测量时间间隔。例如统计请求处理花费的时间等。单调时钟是单节点的,所以比较不同节点上的单调时钟毫无意义。

GO 中时钟的设计

如果你是一个喜欢看 go 源码或者看 go 设计的,肯定会首先看一个包的包说明,go 中 time 包的说明也说得很明白了,它是在一个 time 包内同时提供了墙上时钟与单调时钟。验证也比较简单:

代码语言:javascript
复制
package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Now()
	fmt.Println(t.String())
}

 运行以上代码你会得到类似以下输出(其中 m=+ 前半部分的输出就是墙上时间具体依赖于你的执行环境,而 m=+ 就是单调时钟,单位 s:所以 time.Now() 返回的时间是既包括墙上时间也包括单调时间,具体使用哪一个依赖于你使用的具体方法。

代码语言:javascript
复制
2009-11-10 23:00:00 +0000 UTC m=+0.000000001

单调时钟与墙上时钟

  1. 不受墙上时钟影响的操作

比如经常使用的下面代码计算两个时间差,就是使用的单调时钟。下面的输出将固定为: 1s ,不会因为墙上时间的同步而调整。

代码语言:javascript
复制
package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Now()
	time.Sleep(1 * time.Second)
	end := time.Now()
	fmt.Println(end.Sub(start))
}

 还有一些像 time.Since(start), time.Until(deadline), and time.Now().Before(deadline) 也都不受墙上时钟影响。那么还有哪些规则呢?根据官方文档有如下几个方法:

  2.  t.Add 如果有单调时钟,则会同时将墙上时钟与单调时钟都做计算

  3.  t.AddDate(y,m,d), t.Round(d), t.Truncate(d) 以及 t.ln, t.Local 和 t.UTC 都会丢弃单调时钟而使用墙上时钟

代码语言:javascript
复制
package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Now()
	fmt.Printf("t:\t%s\n", t.String())

	addT := t.Add(1 * time.Second)
	fmt.Printf("add time:\t%s\n", addT.String())

	addDateT := t.AddDate(0, 0, 1)
	fmt.Printf("add date Time:\t%s\n", addDateT.String())

	roundT := t.Round(10 * time.Microsecond)
	fmt.Printf("round time:\t%s\n", roundT.String())

	truncateT := t.Truncate(1 * time.Second)
	fmt.Printf("truncate time:\t%s\n", truncateT.String())

	localT := t.Local()
	fmt.Printf("local time:\t%s\n", localT.String())

	utcT := t.UTC()
	fmt.Printf("utc time:\t%s\n", utcT.String())
	fmt.Printf("add utcT:\t%s", utcT.Add(1*time.Second).String())

}

 运行以上代码你会得到类似下面的验证输出,其中最后一行输出验证了当使用 t.Add 时,如果没有单调时钟得到结果也不会包含单调时钟。

代码语言:javascript
复制
t:	2021-05-19 11:30:44.370092834 +0800 CST m=+0.000077406
add time:	2021-05-19 11:30:45.370092834 +0800 CST m=+1.000077406
add date Time:	2021-05-20 11:30:44.370092834 +0800 CST
round time:	2021-05-19 11:30:44.37009 +0800 CST
truncate time:	2021-05-19 11:30:44 +0800 CST
local time:	2021-05-19 11:30:44.370092834 +0800 CST
utc time:	2021-05-19 03:30:44.370092834 +0000 UTC
add utcT:	2021-05-19 03:30:45.370092834 +0000 UT

 两个时间操作包含:t.After(u), t.Before(u), t.Equal(u) , t.Sub(u) 遵循以下规则:

t 和 u 都具有单调时钟,那么就会使用单调时钟而忽略墙上时钟,如果任何一个不具有单调时钟,则会使用墙上时钟。

代码语言:javascript
复制
package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Now()
	fmt.Printf("t:\t%s\n", t.String())
	u := t.Add(-1 * time.Second)
	fmt.Printf("u:\t%s\n", u.String())
	fmt.Printf("t.After(u):%t\n", t.After(u))

	u = t.AddDate(0, 0, -1)
	fmt.Printf("u:\t%s\n", u.String())
	fmt.Printf("t.After(u):%t\n", t.After(u)
}

 上面代码都会输出正确的结果:

代码语言:javascript
复制
t:	2021-05-19 14:38:36.251903117 +0800 CST m=+0.000053631
u:	2021-05-19 14:38:35.251903117 +0800 CST m=-0.999946369
t.After(u):true
u:	2021-05-18 14:38:36.251903117 +0800 CST
t.After(u):tru

 因为单调时钟只有在单节点甚至是只有在当前进程才有效的,所以 t.GobEncode, t.MarshalBinary, t.MarshalJSON, and t.MarshalText 都会丢弃单调时钟,而 t.Format 没有提供单调时钟解析。同样,构造函数time.Date,time.Parse,time.ParseInLocation和time.Unix,以及解组器t.GobDecode和t.UnmarshalBinary。 t.UnmarshalJSON和t.UnmarshalText始终创建没有单调时钟的时间。

但是请注意 == 操作符却是会比较 Location 和单调时钟的。

代码语言:javascript
复制
package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Now()
	u := t
	fmt.Printf("t:\t%s \nu:\t%s \nt==u:\t%t\n\n", t.String(), u.String(), t == u)

	u = t.AddDate(0, 0, -1).AddDate(0, 0, 1)
	fmt.Printf("t:\t%s \nu:\t%s \nt==u:\t%t\n\n", t.String(), u.String(), t == u)

	t = t.Local()
	u = t.UTC()
	fmt.Printf("t:\t%s \nu:\t%s \nt==u:\t%t\n\n", t.String(), u.String(), t == u)
}

 运行以上代码你会得到类似下面的输出,看到最后3行输出,虽然看着一样但是因为一个是 Local 时区,一个是UTC 时区,比较结果也是不同,感兴趣的可以读下源码

代码语言:javascript
复制
t:	2009-11-10 23:00:00 +0000 UTC m=+0.000000001 
u:	2009-11-10 23:00:00 +0000 UTC m=+0.000000001 
t==u:	true

t:	2009-11-10 23:00:00 +0000 UTC m=+0.000000001 
u:	2009-11-10 23:00:00 +0000 UTC 
t==u:	false

t:	2009-11-10 23:00:00 +0000 UTC 
u:	2009-11-10 23:00:00 +0000 UTC 
t==u:	false

总结

虽然平时很少会注意到这样的细节,对于时钟来说往往是这样的细节导致一些微妙的 bug,比如一些操作有可能不会得到你想得到的结果,比如有些系统会因为系统休眠而停止单调时钟,那么当你使用 t.Sub(u) 时也许并不会得到你想要的结果。考虑在虚拟机中执行代码就有可能出现上述问题。

还有就是一直觉得 go 的源码文档是非常值得 go 学习者学习的,通过对 time 包的文档解读,更能够加深这点。 最后为了方便大家查阅复习,下面附上一个整理的表格:

func

包含单调时钟

包含墙上时钟

备注

time.Now()

y

y

t.Add()

t 包含结果就包含,否则就不包含

y

t.AddDate(y, m, d), t.Round(d), and t.Truncate(d)

n(计算时会抛弃单调时钟)

y

t.In, t.Local, and t.UTC

n

y

t.After(u), t.Before(u), t.Equal(u), and t.Sub(u), t.Before(u)

如果 t 与 u 都包含单调时钟,那么就用单调时钟计算,否则就用墙上时钟计算

t.GobEncode, t.MarshalBinary, t.MarshalJSON, and t.MarshalText

n

y

t.GobDecode, t.UnmarshalBinary. t.UnmarshalJSON, and t.UnmarshalText

n

y

t.Format

n

y

t == u

y

y

不仅会比较单调时钟与墙上时钟,还会比较时区

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言--时钟的重要性
  • 墙上时钟(wall clock) 与单调时钟(monotonic clock)
    • 墙上时钟
      • 单调时钟
      • GO 中时钟的设计
        • 单调时钟与墙上时钟
        • 总结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档