作为程序员,我们经常需要对时间进行处理。在 Go 中,标准库 time 提供了对应的能力。
应对时区问题
在编程中,我们经常会遭遇八小时时间差问题。这是由时区差异引起的,为了能更好地解决它们,我们需要理解几个时间定义标准。
GMT(Greenwich Mean Time),格林威治平时。GMT 根据地球的自转和公转来计算时间,它规定太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间为中午12点。GMT 是前世界标准时。
UTC(Coordinated Universal Time),协调世界时。UTC 比 GMT 更精准,它根据原子钟来计算时间。在不需要精确到秒的情况下,可以认为 UTC=GMT。UTC 是现世界标准时。
从格林威治本初子午线起,往东为正,往西为负,全球共划分为 24 个标准时区,相邻时区相差一个小时。
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println(time.Now())
}
中国大陆使用的是东八时区的标准时,即北京时间 CST,China Standard Time。
$ go run main.go
2022-07-17 16:37:31.186043 +0800 CST m=+0.000066647
这是默认时区下的结果,time.Now()的打印中会标注+0800 CST。
假设我们是在美国洛杉矶时区下,那得到的结果是什么呢?
$ TZ="America/Los_Angeles" go run main.go
2022-07-17 01:39:12.391505 -0700 PDT m=+0.000069514
可以看到,此时的结果是-0700 PDT 时间,即 PDT(Pacific Daylight Time)太平洋夏季时间。由于时区差异,两次执行的时间结果相差了 15 小时。
注意,在使用 Docker 容器时,系统默认的时区就是 UTC 时间(0 时区),和我们实际需要的北京时间相差八个小时,这是导致八小时时间差问题的经典场景。
时区问题的应对策略,可以详细查看 src/time/zoneinfo_unix.go 中 initLocal() 函数的加载逻辑。例如,可以通过指定环境变量 TZ,修改/etc/localtime文件等方式来解决。
因为时区问题非常重要,所以放在了文章第一部分讲述。下面开始介绍 time 库的使用。
时间瞬间 time.Time
time 库,最核心的对象是 time.Time 结构体。它的定义如下,用以表示某个瞬间的时间。
type Time struct {
// wall and ext encode the wall time seconds, wall time nanoseconds,
// and optional monotonic clock reading in nanoseconds.
wall uint64
ext int64
loc *Location
}
计算机在时间处理上,主要涉及到两种时钟。
- 墙上时钟(wall time),又称为钟表时间,用于表示具体的日期与时间。
- 单调时钟(monotonic clocks),总是保证时间是向前的,不会出现墙上时钟的回拨问题,因此它很适合用于测量持续时间段。
wall 和 ext 字段就是用于记录墙上时钟和单调时钟,精度为纳秒。字段的对应位数上关联着用于确定时间的具体年、月、日、小时、分钟、秒等信息。
loc 字段记录时区位置,当 loc 为 nil 时,默认为 UTC 时间。
因为 time.Time 用于表示具有纳秒精度的时间瞬间,在程序中通常应该将它作为值存储和传递,而不是指针。
即在时间变量或者结构体字段中,我们应该使用 time.Time,而非 *time.Time。
获取 time.Time
我们可以通过 Now 函数获取当前本地时间
func Now() Time {}
也可以通过 Date 函数,根据年、月、日等时间和时区参数获取指定时间
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time {}
转换时间戳
计算机世界中,将 UTC 时间 1970 年1月1日 0 时 0 分 0 秒作为 Unix 时间 0。所谓的时间瞬间转换为 Unix 时间戳,即计算的是从 Unix 时间 0 到指定瞬间所经过的秒数、微秒数等。
func (t Time) Unix() int64 {} // 从 Unix 时间 0 经过的秒数
func (t Time) UnixMicro() int64 {} // 从 Unix 时间 0 经过的微秒数
func (t Time) UnixMilli() int64 {} // 从 Unix 时间 0 经过的毫秒数
func (t Time) UnixNano() int64 {} // 从 Unix 时间 0 经过的纳秒数
获取基本字段
t := time.Now()
fmt.Println(t.Date()) // 2022 July 17
fmt.Println(t.Year()) // 2022
fmt.Println(t.Month()) // July
fmt.Println(t.ISOWeek()) // 2022 28
fmt.Println(t.Clock()) // 22 21 56
fmt.Println(t.Day()) // 17
fmt.Println(t.Weekday()) // Sunday
fmt.Println(t.Hour()) // 22
fmt.Println(t.Minute()) // 21
fmt.Println(t.Second()) // 56
fmt.Println(t.Nanosecond())// 494313000
fmt.Println(t.YearDay()) // 198
持续时间 time.Duration
持续时间 time.Duration 用于表示两个时间瞬间 time.Time 之间所经过的时间。它通过 int64 表示纳秒计数,能表示的极限大约为 290 年。
// A Duration represents the elapsed time between two instants
// as an int64 nanosecond count. The representation limits the
// largest representable duration to approximately 290 years.
type Duration int64
在 Go 中,持续时间只是一个以纳秒为单位的数字而已。如果持续时间等于 1000000000,则它代表的含义是 1 秒或 1000 毫秒或 1000000 微秒或 1000000000 纳秒。
例如,相隔 1 小时的两个时间瞬间 time.Time 值,它们之间的持续时间 time.Duration 值为
1*60*60*1000*1000*1000
Go 的 time 包中定义了这些持续时间常量值
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
同时,time.Duration 提供了能获取各时间粒度数值的方法
func (d Duration) Nanoseconds() int64 {} // 纳秒
func (d Duration) Microseconds() int64 {} // 微秒
func (d Duration) Milliseconds() int64 {} // 毫秒
func (d Duration) Seconds() float64 {} // 秒
func (d Duration) Minutes() float64 {} // 分钟
func (d Duration) Hours() float64 {} // 小时
时间计算
在学习了时间瞬间和持续时间之后,我们来看如何做时间计算。
Add 函数用于增加/减少( d 的正值表示增加、负值表示减少) time.Time 的持续时间。我们可以对某瞬时时间,增加或减少指定纳秒级以上的时间。
func (t Time) Add(d Duration) Time {}
AddDate 函数基于年、月和日的维度增加/减少 time.Time 的值
func (t Time) AddDate(years int, months int, days int) Time {}
Sub 函数可以得出两个时间瞬间之间的持续时间(可以计算代码执行时间)。
func (t Time) Sub(u Time) Duration {}
当然,基于当前时间瞬间 time.Now() 的计算是最普遍的需求。因此,time 包还提供了以下便捷的时间计算函数
Since 函数是 time.Now().Sub(t) 的快捷方法。
func Since(t Time) Duration {}
Until 函数是 t.Sub(time.Now()) 的快捷方法。
func Until(t Time) Duration {}
使用示例
t := time.Now()
fmt.Println(t) // 2022-07-17 22:41:06.001567 +0800 CST m=+0.000057466
//时间增加 1小时
fmt.Println(t.Add(time.Hour * 1)) // 2022-07-17 23:41:06.001567 +0800 CST m=+3600.000057466
//时间增加 15 分钟
fmt.Println(t.Add(time.Minute * 15))// 2022-07-17 22:56:06.001567 +0800 CST m=+900.000057466
//时间增加 10 秒钟
fmt.Println(t.Add(time.Second * 10))// 2022-07-17 22:41:16.001567 +0800 CST m=+10.000057466
//时间减少 1 小时
fmt.Println(t.Add(-time.Hour * 1)) // 2022-07-17 21:41:06.001567 +0800 CST m=-3599.999942534
//时间减少 15 分钟
fmt.Println(t.Add(-time.Minute * 15))// 2022-07-17 22:26:06.001567 +0800 CST m=-899.999942534
//时间减少 10 秒钟
fmt.Println(t.Add(-time.Second * 10))// 2022-07-17 22:40:56.001567 +0800 CST m=-9.999942534
time.Sleep(time.Second * 5)
t2 := time.Now()
// 计算 t 到 t2 的持续时间
fmt.Println(t2.Sub(t)) // 5.004318874s
// 1 年之后的时间
t3 := t2.AddDate(1, 0, 0)
// 计算从 t 到当前的持续时间
fmt.Println(time.Since(t)) // 5.004442316s
// 计算现在到明年的持续时间
fmt.Println(time.Until(t3)) // 8759h59m59.999864s
时间比较
可以将待比较的时间格式化成相同格式的字符串,然后使用time的Before, After, Equal 方法即可
package main
import (
"fmt"
"time"
)
func main() {
time1 := time.Now()
fmt.Println(time1)
time2 := time.Now()
time3 := time2.Add(time.Hour * 24)
fmt.Println(time2)
if time1.Before(time3) {
fmt.Println("Before time1小于")
} else if time1.Equal(time3) {
fmt.Println("Equal time1等于")
} else if time1.After(time3) {
fmt.Println("After time1大于")
}
}
package main
import (
"fmt"
"time"
)
func main() {
time1 := "2021-03-21 09:23:29"
time2 := "2021-03-20 08:50:29"
//先把时间字符串格式化成相同的时间类型
t1, _ := time.Parse("2006-01-02 15:04:05", time1)
t2, _ := time.Parse("2006-01-02 15:04:05", time2)
if t1.Before(t2) { //t1小于t2,如果小于,返回true
fmt.Println("Before time1小于")
}
if t1.After(t2) { //t1大于t2,如果大于,返回true
fmt.Println("After time1大于")
}
time3 := "2021-03-20 08:50:29"
time4 := "2021-03-20 08:50:29"
//先把时间字符串格式化成相同的时间类型
t3, _ := time.Parse("2006-01-02 15:04:05", time3)
t4, _ := time.Parse("2006-01-02 15:04:05", time4)
if t3.Equal(t4) {
fmt.Println("Equal true")
}
}
格式化时间
时间类型有一个自带的 Format 方法进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S 而是使用Go语言的诞生时间 2006 年 1 月 2 号 15 点 04 分 05 秒
。 提示:如果想将时间格式化为 12 小时格式,需指定 PM。
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan
// 24小时制
fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
// 12小时制
fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan"))
fmt.Println(now.Format("2006/01/02 15:04"))
fmt.Println(now.Format("15:04 2006/01/02"))
fmt.Println(now.Format("2006/01/02"))
}
运行结果如下:2019-12-12 15:20:52.037 Thu Dec
2019-12-12 03:20:52.037 PM Thu Dec
2019/12/12 15:20
15:20 2019/12/12
2019/12/12
解析字符串格式的时间
Parse 函数可以解析一个格式化的时间字符串并返回它代表的时间。
func Parse(layout, value string) (Time, error)
与 Parse 函数类似的还有 ParseInLocation 函数。
func ParseInLocation(layout, value string, loc *Location) (Time, error)
ParseInLocation 与 Parse 函数类似,但有两个重要的不同之处:
- 第一,当缺少时区信息时,Parse 将时间解释为 UTC 时间,而 ParseInLocation 将返回值的 Location 设置为 loc;
- 第二,当时间字符串提供了时区偏移量信息时,Parse 会尝试去匹配本地时区,而 ParseInLocation 会去匹配 loc。
package main
import (
"fmt"
"time"
)
func main() {
var layout string = "2006-01-02 15:04:05"
var timeStr string = "2019-12-12 15:22:12"
timeObj1, _ := time.Parse(layout, timeStr)
fmt.Println(timeObj1)//2019-12-12 15:22:12 +0000 UTC
timeObj2, _ := time.ParseInLocation(layout, timeStr, time.Local)
fmt.Println(timeObj2)//2019-12-12 15:22:12 +0800 CST
}
时区转换
在文章开头,我们介绍了时区问题。如果在代码中,需要获取同一个 time.Time 在不同时区下的结果,我们可以使用它的 In 方法。
func (t Time) In(loc *Location) Time {}
它的使用非常简单,直接看示例代码
now := time.Now()
fmt.Println(now) // 2022-07-18 21:19:59.9636 +0800 CST m=+0.000069242
loc, _ := time.LoadLocation("UTC")
fmt.Println(now.In(loc)) // 2022-07-18 13:19:59.9636 +0000 UTC
loc, _ = time.LoadLocation("Europe/Berlin")
fmt.Println(now.In(loc)) // 2022-07-18 15:19:59.9636 +0200 CEST
loc, _ = time.LoadLocation("America/New_York")
fmt.Println(now.In(loc)) // 2022-07-18 09:19:59.9636 -0400 EDT
loc, _ = time.LoadLocation("Asia/Dubai")
fmt.Println(now.In(loc)) // 2022-07-18 17:19:59.9636 +0400 +04
定时器
定时器是一种通过设置一项任务,在未来的某个时刻执行该任务的机制。
定时器的种类通常只有两种,一种是只执行一次的延时模式,一种是每隔一段时间执行一次的间隔模式。
在现代编程语言中,定时器几乎是标配。除了设置定时器外,还需要有提供定时器的方法。
比如在 JavaScript 中,提供了 setTimeout、setInterval、clearTimeout 和 clearInterval 四个 API,相比较而言是比较简单的。Go 语言中定时器的 API 就比较完善,所有的 API 都在 time 包中。
延迟执行
延迟执行有两种方式,time.After 和 time.Sleep。
time.After
下面是输出 1 之后间隔 1 秒后再输出 2 的例子。
fmt.Println("1")
timeAfterTrigger := time.After(1 * time.Second) //定义延迟通道
<-timeAfterTrigger //阻塞,延迟1秒
fmt.Println("2")
time.Sleep
相比较而言,time.Sleep 实现同样的效果用起来就更加简洁了。
fmt.Println("1")
time.Sleep(1*time.Second)
fmt.Println("2")
两者的区别在于 time.Sleep 是阻塞当前协程,time.After 基于 channel 实现,可以在不同的协程中传递。
time.AfterFunc
除了 time.After 和 time.Sleep 以外,还有 AfterFunc 函数。作用类似 After,可以在延迟一段时间后触发某个函数。 下面是输出 1 ,然后延迟 1 秒后输出 2 的例子。
fmt.Println("1")
c := make(chan int, 1)
time.AfterFunc(1*time.Second, func() { //定义延迟通道,并在到期后执行指定函数
fmt.Println("2")
c <- 1
})
<-c //阻塞,延迟1秒
定时执行
定时执行又分两种情况,一种是执行 N 次后结束,另一种是程序不终止一直执行。
time.NewTicker
下面是输出 1 之后间每隔 1 秒输出 2,连续输出 5 次后结束的例子。
fmt.Println("1")
count := 0
timeTicker := time.NewTicker(1 * time.Second)
for {
<-timeTicker.C
fmt.Println("2")
count++
if count >= 5 {
timeTicker.Stop() //停止定时器
}
}
控制NewTicker定时器(Stop和Reset)
定时器提供了 Stop 方法和 Reset 方法。
Stop 方法的作用是停止定时器,Reset 方法的作用是改变定时器的间隔时间。
Stop 方法的应用在上面的实例中已经展示了。
time.Reset
下面是一个输出 1 之后每隔 1 秒输出 2 ,输出 3 次 2 后改为每隔 2 秒输出 2 的例子。
fmt.Println("1")
count := 0
timeTicker := time.NewTicker(1 * time.Second)
for {
<-timeTicker.C
fmt.Println("2")
count++
if count >= 3 {
timeTicker.Reset(2 * time.Second) //重置间隔执行时间
}
}
time.Tick
下面是每隔 1 秒输出 2,程序不终止一直循环输出的例子。
t:=time.Tick(1*time.Second) for{
<-t
fmt.Println("每隔1秒输出一次")
}