golang系列之-time

time涉及的内容较多,如生成/存储时间、比较时间、获取时间信息、时区、夏令时等,本文仅介绍一些自己感兴趣的地方

日历计算基于格里高利历(公历),1年有365天(闰年有366天)。同时支持墙上时钟(wall clock)和单调时钟(monotonic clock),其中墙上时钟用于时间同步、报时(time-telling),单调时钟用于时间测量(time-measuring)。并不是所有函数都支持单调时钟,如字符串编码/解码函数就会舍弃单调时钟数据

注意:

  1. 时间精确度:纳秒
  2. 大部分都是线程安全,除了
    • GobDecode
    • UnmarshalBinary
    • UnmarshalJSON
    • UnmarshalText
  3. 有些系统会在进程休眠时停止单调时钟,会导致一些函数计算异常,如
    • Sub
    • Since
    • Until
    • Before
    • After
    • Add
    • Equal
    • Compare
  4. 字符串编码时,保存的是Location的offset,会导致dst-夏令时丢失,相关函数
    • GobEncode
    • MarshalBinary
    • AppendBinary
    • MarshalJSON
    • MarshalText
    • AppendText
  5. 字符串编码/解码时,会丢弃单调时钟信息

当前go版本:1.24

快速上手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
"fmt"
"time"
)

func main() {
p := fmt.Println

// 当前时刻
now := time.Now()
p(now)

// 指定时刻
then := time.Date(
2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
p(then)

p(then.Year()) // 年
p(then.Month()) // 月
p(then.Day()) // 日
p(then.Hour()) // 时
p(then.Minute()) // 分
p(then.Second()) // 秒
p(then.Nanosecond()) // 纳秒
p(then.Location()) // 时区

p(then.Weekday()) // 星期

// 判断两个时刻先后顺序
p(then.Before(now))
p(then.After(now))
p(then.Equal(now))

// 时长/时刻差值
diff := now.Sub(then)
p(diff)

p(diff.Hours()) // 转换成总小时数
p(diff.Minutes()) // 转换成总分钟数
p(diff.Seconds()) // 转换成总秒数
p(diff.Nanoseconds()) // 转换成总纳秒数

p(then.Add(diff))
p(then.Add(-diff))
}

运行结果如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go run main.go

# 2025-02-22 10:30:14.247195 +0800 CST m=+0.000103986 # 当前时刻
# 2009-11-17 20:34:58.651387237 +0000 UTC # 指定时刻
# 2009 # 年
# November # 月
# 17 # 日
# 20 # 时
# 34 # 分
# 58 # 秒
# 651387237 # 纳秒
# UTC # 时区
# Tuesday # 星期
# true
# false
# false
# 133805h55m15.595807763s # 时长/时刻差值
# 133805.9209988355 # 总小时数
# 8.028355259930129e+06 # 总分钟数
# 4.817013155958078e+08 # 总秒数
# 481701315595807763 # 总纳秒数
# 2025-02-22 02:30:14.247195 +0000 UTC
# 1994-08-13 14:39:43.055579474 +0000 UTC

数据结构

时间由Time数据结构表示,具体如下

1
2
3
4
5
6
// src/time/time.go
type Time struct {
wall uint64 // 1位表示flag,33位表示秒数,30位表示纳秒数
ext int64 // wall与ext组合表示:秒数(墙上时钟)+纳秒数(墙上时钟)+纳秒数(单调时钟)【可选】
loc *Location // 时区,为nil时表示UTC
}

时间存储

Time结构使用wall和ext两个字段来存储时间,具体表示如下

  1. 当wall第一位即flag为0时,表示没有单调时钟数据,wall和ext存储内容如下
1
2
3
4
5
// wall -> | 1bit | 33bit                     | 30bit                     |
// | 0 | 0 | def(ns) -> [0, 999999999] |
//
// ext -> | 64bit |
// | abc(s) => int64 |

其中,abc伪数据表示秒数,从1年1月1日开始算起;def伪数据表示纳秒数,范围[0, 999999999]

  1. 当wall第一位为1时,表示有单调时钟数据,wall和ext存储内容如下
1
2
3
4
5
// wall -> | 1bit | 33bit                     | 30bit                     |
// | 1 | abc(s) => unsigned | def(ns) -> [0, 999999999] |
//
// ext -> | 64bit |
// | xyz(ns) => int64 |

其中,abc伪数据表示秒数,def伪数据表示纳秒数,范围[0, 999999999],xyz为单调时钟的纳秒数,从进程启动开始计时

此时,墙上时钟能表示的时间范围是[1885, 2157]

获取时间对象Time

当前时刻Now

Now返回系统时间,返回的Time一般包含单调时钟数据

具体逻辑

  1. 从系统获取时间数据(通过VDSO或clock_gettime获取)
  2. 如果返回的时间没有单调时钟数据,按墙上时钟格式存储
  3. 如果秒数出现溢出的情况,按墙上时钟格式存储
  4. 存储墙上时钟和单调时钟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 返回本地当前时间
func Now() Time {
// 从系统获取时间数据
sec, nsec, mono := runtimeNow()
// 没有单调时间
if mono == 0 {
// unix时间戳存储时都要加上unixToInternal
return Time{uint64(nsec), sec + unixToInternal, Local}
}
// 减去进程启动时获取的nanotime
mono -= startNano
// 检测是否会溢出,2157年3月16日耗尽所有bit
sec += unixToInternal - minWall
// 溢出?丢弃单调时钟,只存墙上时钟
if uint64(sec)>>33 != 0 {
return Time{uint64(nsec), sec + minWall, Local}
}
// 墙上时钟+单调时钟
return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}

// 进程启动时初始化该变量
var startNano int64 = runtimeNano() - 1

// src/runtime/time.go
func time_runtimeNow() (sec int64, nsec int32, mono int64) {
if sg := getg().syncGroup; sg != nil {
sec = sg.now / (1000 * 1000 * 1000)
nsec = int32(sg.now % (1000 * 1000 * 1000))
return sec, nsec, sg.now
}
return time_now()
}

// src/runtime/timestub.go
func time_now() (sec int64, nsec int32, mono int64) {
sec, nsec = walltime() // 通过VDSO或clock_gettime获取
return sec, nsec, nanotime()
}

// src/runtime/time.go
func time_runtimeNano() int64 {
gp := getg()
if gp.syncGroup != nil {
return gp.syncGroup.now
}
return nanotime()
}

// src/runtime/time_nofake.go
func nanotime() int64 {
// linux+amd64看相关汇编代码 src/runtime/sys_linux_amd64.s
return nanotime1() // 通过VDSO或clock_gettime获取
}

Unix时间戳转换

根据秒数、纳秒数返回一个从1970年1月1日开始计时的unix时间戳的Time,该Time不包含单调时钟数据。按参数精度分为Unix/UnixMilli/UnixMicro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 精度:秒
func Unix(sec int64, nsec int64) Time {
// 负数或超过1e9
if nsec < 0 || nsec >= 1e9 {
n := nsec / 1e9
sec += n // 溢出部份换算成秒,加到sec
nsec -= n * 1e9 // 移除溢出部份
if nsec < 0 { // 负数
nsec += 1e9 // 从sec拿1s加上,使其为正
sec-- // sec减去1s
}
}
return unixTime(sec, int32(nsec))
}

func unixTime(sec int64, nsec int32) Time {
// 存储时加上一个预设值unixToInternal,读取的时候减掉
return Time{uint64(nsec), sec + unixToInternal, Local}
}

// 精度:毫秒
func UnixMilli(msec int64) Time {
return Unix(msec/1e3, (msec%1e3)*1e6)
}

// 精度:微秒
func UnixMicro(usec int64) Time {
return Unix(usec/1e6, (usec%1e6)*1e3)
}

Date获取指定时刻

根据提供的时间信息如:年/月/日/时/分/秒以及时区获得指定时刻,具体逻辑如下

  1. 规范化后计算出一个unix时间戳
  2. 根据时区的偏移量调整unix时间戳
  3. 生成Time示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time {
// 时区不能为空
if loc == nil {
panic("time: missing Location in call to Date")
}

// 规范月份,溢出值加到年上
m := int(month) - 1
year, m = norm(year, m, 12)
month = Month(m) + 1

// 规范nsec, sec, min, hour, 溢出值加到天上
sec, nsec = norm(sec, nsec, 1e9)
min, sec = norm(min, sec, 60)
hour, min = norm(hour, min, 60)
day, hour = norm(day, hour, 24)

// 转换为绝对时间,然后转换为unix时间戳
unix := int64(dateToAbsDays(int64(year), month, day))*secondsPerDay +
int64(hour*secondsPerHour+min*secondsPerMinute+sec) +
absoluteToUnix

// 时区偏移量
_, offset, start, end, _ := loc.lookup(unix)
if offset != 0 {
utc := unix - int64(offset)
if utc < start || utc >= end {
_, offset, _, _, _ = loc.lookup(utc)
}
unix -= int64(offset)
}

// unix时间戳
t := unixTime(unix, int32(nsec))
t.setLoc(loc)
return t
}

// hi * base + lo == nhi * base + nlo
// 0 <= nlo < base
func norm(hi, lo, base int) (nhi, nlo int) {
if lo < 0 {
n := (-lo-1)/base + 1
hi -= n
lo += n * base
}
if lo >= base {
n := lo / base
hi += n
lo -= n * base
}
return hi, lo
}

// 转换为天数
func dateToAbsDays(year int64, month Month, day int) absDays {
amonth := uint32(month)
janFeb := uint32(0)
if amonth < 3 {
janFeb = 1
}
// 如果是1月和2月,再加12个月
amonth += 12 * janFeb
// year-janFeb+292277022400
y := uint64(year) - uint64(janFeb) + absoluteYears

// 如果3 <= amonth <= 14
// ayday := (153*amonth - 457) / 5
// 等同于
// ayday := (979*amonth - 2919) >> 5
ayday := (979*amonth - 2919) >> 5

century := y / 100
cyear := uint32(y % 100)
cday := 1461 * cyear / 4
centurydays := 146097 * century / 4

// absDays => uint64的别名
return absDays(centurydays + uint64(int64(cday+ayday)+int64(day)-1))
}

增加时长获取指定时刻

Add

时刻t增加时长d,获得指定时刻,该方法会同时更新墙上时钟和单调时钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
func (t Time) Add(d Duration) Time {
dsec := int64(d / 1e9) // 秒数
nsec := t.nsec() + int32(d%1e9) // 纳秒数
if nsec >= 1e9 {
dsec++
nsec -= 1e9
} else if nsec < 0 {
dsec--
nsec += 1e9
}
t.wall = t.wall&^nsecMask | uint64(nsec) // 更新纳秒数
t.addSec(dsec)
// 单调时钟
if t.wall&hasMonotonic != 0 {
te := t.ext + int64(d)
if d < 0 && te > t.ext || d > 0 && te < t.ext {
// 单调时钟溢出,降级成墙上时钟
t.stripMono()
} else {
t.ext = te
}
}
return t
}

func (t *Time) addSec(d int64) {
// 单调时钟
if t.wall&hasMonotonic != 0 {
sec := int64(t.wall << 1 >> (nsecShift + 1))
dsec := sec + d
if 0 <= dsec && dsec <= 1<<33-1 {
t.wall = t.wall&nsecMask | uint64(dsec)<<nsecShift | hasMonotonic
return
}
// 秒数溢出,把单调时钟数据移除,降级为墙上时钟
t.stripMono()
}

// 墙上时钟
sum := t.ext + d
if (sum > t.ext) == (d > 0) {
t.ext = sum
} else if d > 0 {
t.ext = 1<<63 - 1
} else {
t.ext = -(1<<63 - 1)
}
}

// 移除mono时钟数据
func (t *Time) stripMono() {
if t.wall&hasMonotonic != 0 {
t.ext = t.sec() // 复制秒数到ext
t.wall &= nsecMask // 移除flag
}
}

AddDate

Time根据提供的年数/月数/日数获得目标指定时刻,底层实际调用的是Date方法,只更新wall clock

1
2
3
4
5
func (t Time) AddDate(years int, months int, days int) Time {
year, month, day := t.Date()
hour, min, sec := t.Clock()
return Date(year+years, month+Month(months), day+days, hour, min, sec, int(t.nsec()), t.Location())
}

字符串转换成Time

常用的layout有:

constant value
DateTime “2006-01-02 15:04:05”
DateOnly “2006-01-02”
TimeOnly “15:04:05”

Parse

解析字符串获得Time对象以及异常信息

1
2
3
4
5
6
7
8
9
10
func Parse(layout, value string) (Time, error) {
// RFC3339 => 2006-01-02T15:04:05Z07:00
if layout == RFC3339 || layout == RFC3339Nano {
if t, ok := parseRFC3339(value, Local); ok {
return t, nil
}
}
// parse函数太长了,有兴趣可以看源码
return parse(layout, value, UTC, Local)
}

ParseInLocation

指定时区,解析字符串获得Time对象以及异常信息

1
2
3
4
5
6
7
8
9
10
func ParseInLocation(layout, value string, loc *Location) (Time, error) {
// RFC3339 => 2006-01-02T15:04:05Z07:00
if layout == RFC3339 || layout == RFC3339Nano {
if t, ok := parseRFC3339(value, loc); ok {
return t, nil
}
}
// parse函数太长了,有兴趣可以看源码
return parse(layout, value, loc, loc)
}

计算两个时刻的差值

Sub

计算t时刻-u时刻的差值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func (t Time) Sub(u Time) Duration {
// 两个时刻都有单调时钟
if t.wall&u.wall&hasMonotonic != 0 {
return subMono(t.ext, u.ext)
}
// 秒数+纳秒数
d := Duration(t.sec()-u.sec())*Second + Duration(t.nsec()-u.nsec())

switch {
case u.Add(d).Equal(t):
return d // d is correct
case t.Before(u):
return minDuration // t - u is negative out of range
default:
return maxDuration // t - u is positive out of range
}
}

func subMono(t, u int64) Duration {
d := Duration(t - u)
if d < 0 && t > u {
return maxDuration // t - u is positive out of range
}
if d > 0 && t < u {
return minDuration // t - u is negative out of range
}
return d
}

Since

计算从指定时刻(过去)到现在时刻的差值

1
2
3
4
5
6
7
func Since(t Time) Duration {
// 目标时刻有单调时钟数据
if t.wall&hasMonotonic != 0 {
return subMono(runtimeNano()-startNano, t.ext)
}
return Now().Sub(t)
}

Until

计算从现在时刻到指定时刻(将来)的差值

1
2
3
4
5
6
7
func Until(t Time) Duration {
// 目标时刻有单调时钟数据
if t.wall&hasMonotonic != 0 {
return subMono(t.ext, runtimeNano()-startNano)
}
return t.Sub(Now())
}

比较两个时刻的先后顺序

如果两个Time都包含wall clock和mono clock,则只比较mono clock,否则比较wall clock

Before

判断t时刻是否先于u时刻

1
2
3
4
5
6
7
8
func (t Time) Before(u Time) bool {
if t.wall&u.wall&hasMonotonic != 0 {
return t.ext < u.ext
}
ts := t.sec()
us := u.sec()
return ts < us || ts == us && t.nsec() < u.nsec()
}

After

判断t时刻是否晚于u时刻

1
2
3
4
5
6
7
8
func (t Time) After(u Time) bool {
if t.wall&u.wall&hasMonotonic != 0 {
return t.ext > u.ext
}
ts := t.sec()
us := u.sec()
return ts > us || ts == us && t.nsec() > u.nsec()
}

Equal

判断t时刻与u时刻是否相等

注意:时刻比较不要使用==,因为==会比较整个Time结构

1
2
3
4
5
6
func (t Time) Equal(u Time) bool {
if t.wall&u.wall&hasMonotonic != 0 {
return t.ext == u.ext
}
return t.sec() == u.sec() && t.nsec() == u.nsec()
}

IsZero

判断t时刻是否是零值。

注意:零值是UTC时间1年1月1日0时0分0秒

1
2
3
func (t Time) IsZero() bool {
return t.sec() == 0 && t.nsec() == 0
}

Compare

比较两个时刻

  1. 如果t时刻先于u时刻,返回-1
  2. 如果t时刻晚与u时刻,返回+1
  3. 如果t时刻与u时刻相等,返回0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (t Time) Compare(u Time) int {
var tc, uc int64
if t.wall&u.wall&hasMonotonic != 0 {
tc, uc = t.ext, u.ext
} else {
tc, uc = t.sec(), u.sec()
if tc == uc {
tc, uc = int64(t.nsec()), int64(u.nsec())
}
}
switch {
case tc < uc:
return -1
case tc > uc:
return +1
}
return 0
}

时区

转换时区

时区不会改变Time存储的数值,影响的是数据输出/表示

Local

切换为本机时区

1
2
3
4
5
6
7
8
9
10
11
12
13
func (t Time) Local() Time {
t.setLoc(Local)
return t
}

func (t *Time) setLoc(loc *Location) {
// nil表示UTC
if loc == &utcLoc {
loc = nil
}
t.stripMono()
t.loc = loc
}

UTC

切换为UTC时区

1
2
3
4
5
6
var utcLoc = Location{name: "UTC"} // 启动时初始化

func (t Time) UTC() Time {
t.setLoc(&utcLoc)
return t
}

指定时区

切换为指定时区

1
2
3
4
5
6
7
func (t Time) In(loc *Location) Time {
if loc == nil {
panic("time: missing Location in call to Time.In")
}
t.setLoc(loc)
return t
}

获取报时信息

获取Unix时间戳

获取从1970年1月1日开始到现在的秒数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 精度:秒
func (t Time) Unix() int64 {
return t.unixSec()
}

// 精度:毫秒
func (t Time) UnixMilli() int64 {
return t.unixSec()*1e3 + int64(t.nsec())/1e6
}

// 精度:微秒
func (t Time) UnixMicro() int64 {
return t.unixSec()*1e6 + int64(t.nsec())/1e3
}

// 精度:纳秒
func (t Time) UnixNano() int64 {
return (t.unixSec())*1e9 + int64(t.nsec())
}

// 包含秒数和纳秒数
// unix时间戳读取时都要加上internalToUnix(也就是减去unixToInternal)
func (t *Time) unixSec() int64 { return t.sec() + internalToUnix }

提取日期等信息

业务逻辑经常需要判断两个时刻是否处在同一天/同一周等,此外,也需要获取当前周/当前月等的起止时刻,列举如下依赖方法

method desc
Date 获取年月日
Clock 获取时分秒
ISOWeek 获取年和周数
Year 获取年
Month 获取月
Day 获取天
Hour 获取时
Minute 获取分
Second 获取秒
Round 获取秒
Truncate 按给定时长截断
Round 按给定时长向上取整

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. 两个时刻是否是同一天
NewDate(t1.Date()) == NewDate(t2.Date())

type Date struct {
YearMonth
day int
}

func NewDate(year int, month time.Month, day int) Date {
return Date{
YearMonth: YearMonth{
year: year,
month: month,
},
day: day,
}
}

// 2. 获取当月开始和结束时刻
startOfMonth := time.Date(t1.Year(), t1.Month(), 1, 0, 0, 0, 0, t1.Location())
endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-1 * time.Second)

// 3. 获取当个星期的开始和结束时刻
// 从星期天开始计起
tmp := t1.Truncate(7 * 24 * time.Hour)
startOfWeek := time.Date(tmp.Year(), tmp.Month(), tmp.Day(), 0, 0, 0, 0, tmp.Location())
endOfWeek := startOfWeek.AddDate(0, 0, 7).Add(-1 * time.Second)