go time 包关于时钟的理解

2022-09-29 15:41:06 浏览数 (2)

| 导语 最近看书再次看到了墙上时钟与单调时钟,瞬间勾起了对 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%sn", t.String())

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

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

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

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

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

	utcT := t.UTC()
	fmt.Printf("utc time:t%sn", 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%sn", t.String())
	u := t.Add(-1 * time.Second)
	fmt.Printf("u:t%sn", u.String())
	fmt.Printf("t.After(u):%tn", t.After(u))

	u = t.AddDate(0, 0, -1)
	fmt.Printf("u:t%sn", u.String())
	fmt.Printf("t.After(u):%tn", 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%tnn", 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%tnn", t.String(), u.String(), t == u)

	t = t.Local()
	u = t.UTC()
	fmt.Printf("t:t%s nu:t%s nt==u:t%tnn", 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

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

0 人点赞