深入理解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.wallTime.nesc=Time.ext,下面说明以 go1.9之后变量名为准。

时间点关系说明

一个 Time变量表示的是一个标准的 Unix 时间点以及时区信息,Time.wallTime.ext处理没有歧义的时间值, Time.loc处理代表的时区相对于UTC 时间的偏移(其只代表时区,实际偏移量处理由方法计算)。由于 Time.walltype 为 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()

参考文章

深入理解Go时间处理(time.Time)

百度百科

UNIX中的时区TZ设置

posted @ 2020-10-22 14:47  wangbs95  阅读(609)  评论(0编辑  收藏  举报