Go如何处理时间
参考
《Go语言入门经典》23章 Go语言时间编程
理解Golang的Time结构
深入理解GO时间处理(time.Time)
golang 定时任务方面time.Sleep和time.Tick的优劣对比
官方文档time包
开发中经常会使用时间函数,如测试程序的运行时间。Golang
提供了标准库 time
,提供了一系列时间处理的方法。
0 从一个例子开始
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println(time.Now())
fmt.Println(time.Now().Date())
}
// 2021-12-16 21:02:30.094398 +0800 CST m=+0.000321334
// 2021 December 16
常见时间单位换算:
1秒=1000毫秒(ms)
1秒=1,000,000 微秒(μs)
1秒=1,000,000,000 纳秒(ns)
上述代码利用 time
打印了当前时间和日期,其中打印日期还用到了类似链式编程的方法,打印出来的时间是什么意思呢?内部是如何实现的呢?下面对此进行分析。
1 基础篇-时间对象
1.1 Time结构体
1.1.1 before go 1.9
type Time struct {
// sec gives the number of seconds elapsed since
// January 1, year 1 00:00:00 UTC.
sec int64
// nsec specifies a non-negative nanosecond
// offset within the second named by Seconds.
// It must be in the range [0, 999999999].
nsec int32
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// The nil location means UTC.
// All UTC times are represented with loc==nil, never loc==&utcLoc.
loc *Location
}
sec
表示从公元 1年1月1日00:00:00 UTC
到要表示的整数秒数,nsec
表示余下的纳秒数,loc
表示时区。sec
和nsec
处理没有歧义的时间值, loc
处理偏移量
1.1.2 after go 1.9
因为 2017 年闰一秒, 国际时钟调整, Go 程序两次取time.Now()
相减的时间差得到了意料之外的负数, 导致 cloudFlare 的 CDN 服务中断, 详见**https://blog.cloudflare.com/how-and-why-the-leap-second-affected-cloudflare-dns/**, go1.9 在不影响已有应用代码的情况下修改了time.Time的实现。go1.9 的 time.Time 定义为
type Time struct {
// wall and ext encode the wall time seconds, wall time nanoseconds,
// and optional monotonic clock reading in nanoseconds.
//
// From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
// a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
// The nanoseconds field is in the range [0, 999999999].
// If the hasMonotonic bit is 0, then the 33-bit field must be zero
// and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
// If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
// unsigned wall seconds since Jan 1 year 1885, and ext holds a
// signed 64-bit monotonic clock reading, nanoseconds since process start.
wall uint64
ext int64
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// The nil location means UTC.
// All UTC times are represented with loc==nil, never loc==&utcLoc.
loc *Location
}
Time
结构体主要包含三个字段:
- wall
- ext
- loc

Go语言的Time中存储了两种时钟:
- Wall Clocks 用来报时,表示墙上挂的钟,即我们平时理解的时间,存储的形式是自 1885 年 1 月 1 日 0 时 0 分 0 秒以来的时间戳(不像很多语言保存的是Unix时间戳即表示到 1970 年 1 月 1 日)。关于这个 1885 年怎么来的,因为 Go 是可以表示超过 int32 的 unixtimestamp 的时间的,1885 应该是扩展出来的能表示的最小值。
- Monotonic Clocks 用来测量时间,单调时间,这个时间是自进程启动以来的时间戳,只会增长
tick := time.Tick(1 * time.Second)
for range tick {
fmt.Println(time.Now())
}
// 2021-12-17 16:57:45.84885 +0800 CST m=+1.007167459
// 2021-12-17 16:57:46.848403 +0800 CST m=+2.006741001
// 2021-12-17 16:57:47.846837 +0800 CST m=+3.005196626
// 2021-12-17 16:57:48.847365 +0800 CST m=+4.005746209
// 2021-12-17 16:57:49.848387 +0800 CST m=+5.006789584
// 2021-12-17 16:57:50.848322 +0800 CST m=+6.006746042
1.2 Location结构体
// A Location maps time instants to the zone in use at that time.
// Typically, the Location represents the collection of time offsets
// in use in a geographical area. For many Locations the time offset varies
// depending on whether daylight savings time is in use at the time instant.
type Location struct {
name string
zone []zone
tx []zoneTrans
// The tzdata information can be followed by a string that describes
// how to handle DST transitions not recorded in zoneTrans.
// The format is the TZ environment variable without a colon; see
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html.
// Example string, for America/Los_Angeles: PST8PDT,M3.2.0,M11.1.0
extend string
// Most lookups will be for the current time.
// To avoid the binary search through tx, keep a
// static one-element cache that gives the correct
// zone for the time when the Location was created.
// if cacheStart <= t < cacheEnd,
// lookup can return cacheZone.
// The units for cacheStart and cacheEnd are seconds
// since January 1, 1970 UTC, to match the argument
// to lookup.
cacheStart int64
cacheEnd int64
cacheZone *zone
}
要消除时区的影响,可参照世界标准时间(Coordinated Universal Time,UTC)
。UTC是时间标准而非时区,它让不同地区的计算机有相同的参照物,而不用考虑相对时区。
如果本地时间比 UTC 时间快,例如中国的时间比 UTC 快 8 小时,就会写作 UTC+8,俗称东八区。相反,如果本地时间比 UTC 时间慢,例如夏威夷的时间比 UTC 时间慢 10 小时,就会写作 UTC-10,俗称西十区。中国虽然横跨 5 个时区(东五~东九),但是按照北京时间进行统一(东八)。
2 入门篇-时间操作
时间操作的方法非常多,建议参考官方文档time
下面主要针对经常使用的时间操作进行介绍:
2.1 time.Now()获取当前时间
fmt.Println(time.Now())
// 2021-12-18 14:21:15.653623 +0800 CST m=+0.000057626
time.Now()
返回本地时间,以上结果看出:time.Now()
输出默认CST
时区时间,Local
是东八区的时间所以是+0800
,CST
是中部标准时间在中国是以东八区为标准,m=+0.000057626
表示在当前进程的运行时间。

2.2 设置时区
loc, _ := time.LoadLocation("America/New_York")
fmt.Println(loc)
fmt.Println(time.Now().In(loc))
// America/New_York
// 2021-12-18 02:25:26.68076 -0500 EST
LoadLocation(name string) (*Location, error)
可以设置时区,获得一个时区对象。
In(loc *Location) Time
可以传入一个时区对象返回该时区的时间。
2.3 时间运算
时间运算的前提是两个时间属于同一时区。
a, b := time.Now(), time.Now().Add(2*time.Second) // 时间加减
fmt.Println(a)
fmt.Println(b)
fmt.Println(a.After(b)) // a是否在b之后
fmt.Println(a.Before(b)) // a是否在b之后前
// 2021-12-18 15:38:29.399566 +0800 CST m=+0.000076168
// 2021-12-18 15:38:31.399567 +0800 CST m=+2.000076334
// false
// true
2.4 序列化与反序列化
format := "20060102150405"
t1 := time.Now()
fmt.Println(t1) // 2021-12-18 15:57:42.834868 +0800 CST m=+0.000058543
t2 := t1.Format(format) // 20211218155742
fmt.Println(t2)
t3, _ := time.Parse(format, t2)
fmt.Println(t3) // 2021-12-18 15:57:42 +0000 UTC
// format := "2006-01-02 15:04:05"
// 2021-12-18 15:58:38.113167 +0800 CST m=+0.000058292
// 2021-12-18 15:58:38
// 2021-12-18 15:58:38 +0000 UTC
// format := "2006/01/02 Monday January 03:04:05"
// 2021-12-18 16:03:15.01808 +0800 CST m=+0.000077043
// 2021/12/18 Saturday December 04:03:15
// 2021-12-18 04:03:15 +0000 UTC
// format := "2006年1月2日15点4分5秒 Mon Jan"
// 2021-12-18 16:09:02.987355 +0800 CST m=+0.000073418
// 2021年12月18日16点9分2秒 Sat Dec
// 2021-12-18 16:09:02 +0000 UTC
在按照指定格式打印时间时,首先需要记住一个时间 Mon Jan 2 15:04:05 -0700 MST 2006
即2006年1月2日下午3点4分5秒 星期一
,这实际上是凑出来的时间,可能是为了方便记忆,但是为什不直接用yyyy-MM-dd HH:mm:ss
呢?
有博客说这是golang开源的时间,这是错的,golang 的生日是 2009 年 11 月 10 日。
3 提升篇-定时器
定时器在开发中用的很多,尤其是并发编程中,context 包中也有用到(超时控制)。
3.1 time.After()
After(d Duration) <-chan Time
一般用来控制超时,开一个协程如果运行时间超过 1 秒,select 就会收到 After 通道的数据,将不再接收该协程的结果。
ch := make(chan string)
go func() {
time.Sleep(time.Second * 2)
ch <- "result"
}()
select {
case res := <-ch:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("Timeout!")
}
3.2 time.Tick(d time.Duration)
3.2.1 Tick
使用ticker
可让代码每隔特定的时间就重复执行一次。需要在很长的时间内定期执行任务时,这么做很有用,调用Tick
函数会返回一个时间类型的channel
。
ticker := time.Tick(1 * time.Second)
for t := range ticker {
fmt.Println(t)
}
// 2021-12-18 16:36:00.083872 +0800 CST m=+1.001646043
// 2021-12-18 16:36:01.08731 +0800 CST m=+2.005127543
// 2021-12-18 16:36:02.087241 +0800 CST m=+3.005102168
// 2021-12-18 16:36:03.087233 +0800 CST m=+4.005137335
// ...
time.Tick
是对NewTicker
的封装,也可以直接使用NewTicker
。

3.2.2 Sleep
实际上也可以使用time.Sleep(d time.Duration)
来实现循环执行的定时任务:
for {
time.Sleep(1 * time.Second)
fmt.Println(time.Now())
}
那么两种实现方式之间有什么区别呢?哪种效率更高?
3.2.3 Tick vs Sleep
先说结论
调用Tick
函数会返回一个时间类型的channel
,如果对channel
稍微有些了解的话,我们首先会想到,既然是返回一个channel
,在调用 Tick
方法的过程中,必然创建了goroutine
,该goroutine
负责发送数据,唤醒被阻塞的定时任务。
Tick
,Sleep
,包括time.After
函数,都使用的timer
结构体,都会被放在同一个协程中统一处理,这样看起来使用Tick
,Sleep
并没有什么区别。实际上是有区别的,Sleep
是使用睡眠完成定时任务,需要被调度唤醒。Tick
函数是使用channel
阻塞当前协程,完成定时任务的执行。当前并不清楚golang
阻塞和睡眠对资源的消耗会有什么区别,这方面不能给出建议。
但是使用channel
阻塞协程完成定时任务比较灵活,可以结合select
设置超时时间以及默认执行方法,而且可以设置timer
的主动关闭,以及不需要每次都生成一个timer
(这方面节省系统内存,垃圾收回也需要时间)。
所以,建议使用time.Tick
完成定时任务。
实现原理对比
有空再补,先参考