深入理解Go时间设计(time.Time)
前言
时间包括时间值和时区, 没有包含时区信息的时间是不完整的、有歧义的. 和外界传递或解析时间数据时, 应当像HTTP协议或unix-timestamp那样, 使用没有时区歧义的格式, 如果使用某些没有包含时区的非标准的时间表示格式(如yyyy-mm-dd HH:MM:SS), 是有隐患的, 因为解析时会使用场景的默认设置, 如系统时区, 数据库默认时区可能引发事故. 确保服务器系统、数据库、应用程序使用统一的时区, 如果因为一些历史原因, 应用程序各自保持着不同时区, 那么编程时要小心检查代码, 知道时间数据在使用不同时区的程序之间交换时的行为。
Time 结构
go1.9之前
time.Time的定义为
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
}
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.sec
=Time.wall
,Time.nesc
=Time.ext
,下面说明以 go1.9之后变量名为准。
一个 Time
变量表示的是一个标准的 Unix 时间点以及时区信息,Time.wall
和Time.ext
处理没有歧义的时间值, Time.loc
处理代表的时区相对于UTC 时间的偏移(其只代表时区,实际偏移量处理由方法计算)。由于 Time.wall
type 为 uint64
,所以 Time 不能表示 A点以前的时间点(即公元前)。
以图为例,现有一个表示时间点 D 的 Time 变量,它 的
Time.wall
表示从公元1年1月1日00:00:00UTC(点 A)到点 D的整数秒数,Time.ext
表示余下的纳秒数,Time.loc
表示时区。
时间戳
// /usr/local/go/src/time/time.go:1127
func (t Time) Unix() int64 {
return t.unixSec()
}
// /usr/local/go/src/time/time.go:176
// unixSec returns the time's seconds since Jan 1 1970 (Unix time).
func (t *Time) unixSec() int64 { return t.sec() + internalToUnix }
const (
// The unsigned zero year for internal calculations.
// Must be 1 mod 400, and times before it will not compute correctly,
// but otherwise can be changed at will.
absoluteZeroYear = -292277022399
// The year of the zero Time.
// Assumed by the unixToInternal computation below.
internalYear = 1
// Offsets to convert between internal and absolute or Unix times.
absoluteToInternal int64 = (absoluteZeroYear - internalYear) * 365.2425 * secondsPerDay
internalToAbsolute = -absoluteToInternal
unixToInternal int64 = (1969*365 + 1969/4 - 1969/100 + 1969/400) * secondsPerDay
// /usr/local/go/src/time/time.go:418
internalToUnix int64 = -unixToInternal
wallToInternal int64 = (1884*365 + 1884/4 - 1884/100 + 1884/400) * secondsPerDay
internalToWall int64 = -wallToInternal
)
通过Time.Unix()
方法可以获取到代表某个时间点的 Time 变量的时间戳(单位:秒),值得注意的是,时间戳是从 1970 年 1 月 1 日(时间点 C) 到 Time 代表时间点的时间差。
以图为例,D 点时间戳计算过程是
\[D.Unix()=D.wall-unixToInternal \]而
\[unixToInternal=C.wall-A.wall \]显然点 D 与点 C 的差为正值,而点 B 与点 C 的差是负值。所以此方法返回值 type 是
int64
。(1970 年之前的时间戳是负值)
与时间戳有关的 Time 行为
func TimeFeature() {
zeroTime := time.Time{}
fmt.Println("############## zeroTime ################")
fmt.Println("是否是零值:", zeroTime.IsZero())
fmt.Println("时间戳:", zeroTime.Unix())
fmt.Println("格式化输出", zeroTime.String())
fmt.Println()
time1970,_:=time.Parse("2006-01-02","1970-01-01")
fmt.Println("############## time1970 ################")
fmt.Println("是否是零值:", time1970.IsZero())
fmt.Println("时间戳:", time1970.Unix())
fmt.Println("格式化输出", time1970.String())
fmt.Println()
timeAfter1970,_:=time.Parse("2006-01-02","2020-10-22")
fmt.Println("############## timeAfter1970 ################")
fmt.Println("是否是零值:", timeAfter1970.IsZero())
fmt.Println("时间戳:", timeAfter1970.Unix())
fmt.Println("格式化输出", timeAfter1970.String())
fmt.Println()
timeBefore1970,_:=time.Parse("2006-01-02","1930-10-22")
fmt.Println("############## timeBefore1970 ################")
fmt.Println("是否是零值:", timeBefore1970.IsZero())
fmt.Println("时间戳:", timeBefore1970.Unix())
fmt.Println("格式化输出", timeBefore1970.String())
fmt.Println()
}
输出:
=== RUN TestTimeFeature
############## zeroTime ################
是否是零值: true
时间戳: -62135596800
格式化输出 0001-01-01 00:00:00 +0000 UTC
############## time1970 ################
是否是零值: false
时间戳: 0
格式化输出 1970-01-01 00:00:00 +0000 UTC
############## timeAfter1970 ################
是否是零值: false
时间戳: 1603324800
格式化输出 2020-10-22 00:00:00 +0000 UTC
############## timeBefore1970 ################
是否是零值: false
时间戳: -1236902400
格式化输出 1930-10-22 00:00:00 +0000 UTC
--- PASS: TestTimeNil (0.00s)
时区
关于时区概念请阅读百度百科
关于 Unix 时区设置、查看,请参阅UNIX中的时区TZ设置
func TimeZoneFeature() {
timeAfter1970, _ := time.Parse("2006-01-02", "2020-10-22")
fmt.Println("############## UTC ################")
fmt.Println("格式化输出", timeAfter1970.String())
fmt.Println()
fmt.Println("############## Local(CST) ################")
timeAfter1970Local:=timeAfter1970.Local()
fmt.Println("格式化输出", timeAfter1970Local.String())
fmt.Println()
fmt.Println("############## Local(CST)(Now) ################")
timeNow:=time.Now()
fmt.Println("格式化输出", timeNow.String())
fmt.Println()
}
输出:
=== RUN TestTimeZoneFeature
############## UTC ################
格式化输出 2020-10-22 00:00:00 +0000 UTC
############## Local(CST) ################
格式化输出 2020-10-22 08:00:00 +0800 CST
############## Local(CST)(Now) ################
格式化输出 2020-10-22 14:24:14.182418 +0800 CST m=+0.000724561
--- PASS: TestTimeZoneFeature (0.00s)
PASS
现象:
- 前两者比较可得,同一个时间点,设置了不同的时区(
time.Parse()
默认 UTC),格式化输出即存在时间差。 time.Now()
获取到的时间时区是当前机器的时区。
相关源码:
// /usr/local/go/src/time/time.go:1066
// Now returns the current local time.
func Now() Time {
sec, nsec, mono := now()
mono -= startNano
sec += unixToInternal - minWall
if uint64(sec)>>33 != 0 {
// Local 为获取到的代码运行机器设置的时区
return Time{uint64(nsec), sec + minWall, Local}
}
return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}
时区变量 loc 主要在 Time 与字符串的相互转化中起作用,对应方法有 time.Time.Format()
和 time.Parse()