一. 准备知识
在分析Time Struct之前,我们需要先了解两个概念
- wall time: 就是挂在墙上的时钟,我们在计算机中能看到的当前时间就是 wall time,比如'2020-10-10 10:00:00'
- monotonic time: 从字面意思是单调递增的时间,从os启动开始从0计数,重启后重新开始从0开始,单调递增。
按照我们对时间的理解,只需要wall time就够了,为什么还需要monotonic time,难道wall time不是单调递增的。我们来看一起事故:CloudFlare的CDN服务中断.从文中事故描述来看,Now()两次读取当前时间,计算时间差: 后一次Now - 前一次Now,计算结果为负值,然后CloudFlare DNS的代码没有考虑这种情况,然后程序程序跪了。为啥后一次时间-前一次时间为负值???
要解释这个bug产生的原因,首先我们得了解一个概念:闰秒。从wiki摘抄一段: 当要增加正闰秒时,这一秒是增加在第二天的00:00:00之前,效果是延缓UTC第二天的开始。当天23:59:59的下一秒被记为23:59:60,然后才是第二天的00:00:00。这个我们就能理解了,"23:59:60" 在golang里面就是第二天的"00:00:00", 但是过了1s后,依然是"00:00:00", 那么这个时间前后一秒的时间相减,是有可能是负数的,这就是这个问题出现的原因了。那如何解决这个问题呢?对没错,就是monotonic time。golang 在1.9的实现中,加入了解决方案。
好了,背景知识差不多了,我们直接进入golang 源码。本次分析基于的是golang 1.15
二. Time Struct
首先我们看下Time Struct
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 Struct 主要包含三个字段:wall, ext, loc. 从代码注释和源码分析,我们先来分析前两个字段,loc 时区单独列一节探讨。 来看下wall, ext 的存储结构,从源码注释和源码我们看到wall和ext的存储结构分为两种情况:
第一种情况:hasMonotonic = 1
上图是存储图。hasMonotonic标志位存储在wall字段的最高位,为1,wall的64~31位存储从1985-01-01 00:00:00到当前时间的秒数,wall的1-30位存储当前时间的纳秒数。ext字段存储:进程启动到当前时间点的纳秒数.不同的系统获取该值的方式不同,它是系统的调用的返回值. 这个值就是monotonic time,独立于wall time,单调递增。
第二种情况: hasMonotonic = 0
上图是存储图。hasMonotonic标志位存储在wall字段的最高位,为0,顾名思义,就是wall 和 ext中不包含monotonic time。wall的64~31位永远为0,wall的1-30位存储当前时间的纳秒数。ext字段存储:从01-01-01 00:00:00到当前时间的秒数.
那么为什么这么设计,这种设计能解决上面说到的"时间倒流"的问题么? 直接上代码 首先看下Now函数和Sub的实现
func Now() Time {
sec, nsec, mono := now()
mono -= startNano
sec += unixToInternal - minWall
if uint64(sec)>>33 != 0 { // 时间晚于2157 33位存储秒数存不下了
return Time{uint64(nsec), sec + minWall, Local}
}
return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}
func (t Time) Sub(u Time) Duration {
if t.wall&u.wall&hasMonotonic != 0 {
te := t.ext
ue := u.ext
d := Duration(te - ue)
if d < 0 && te > ue {
return maxDuration // t - u is positive out of range
}
if d > 0 && te < ue {
return minDuration // t - u is negative out of range
}
return d
}
d := Duration(t.sec()-u.sec())*Second + Duration(t.nsec()-u.nsec())
// Check for overflow or underflow.
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
}
}
从代码我们可以看出,Now函数wall 和 ext 上面列举的两种存储格式都有可能
- 当前时间晚于2157年的某个时间点,即 uint64(sec)>>33 != 0 为true 采用第二种(hasMonotonic = 0)存储
- 当前时间早于2157年的某个时间点,即 uint64(sec)>>33 != 0 为false 采用第二种(hasMonotonic = 1)存储,按照我们目前时间点来看,我们基本处于这种时间区间,下面我们主要讨论 hasMonotonic = 1 这种格式下的Sub函数的逻辑。
当hasMonotonic = 1这种存储模式下,两次now函数的返回值会使用ext 字段相减来完成,而ext存储着monotonic time,是单调递增的,所以不会出现两次now()相减出现负数的情况,也就解决了时间倒流的情况。Sub()后面的逻辑和时区有关系,下一节单独分析。
创建时间不仅可以通过Now()来创建,我们来看下另一个创建时间的函数time.Date()
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")
}
// Normalize month, overflowing into year.
// 规格化,比如2020-13-01 会规格化为2021-01-01,下面的同理
m := int(month) - 1
year, m = norm(year, m, 12)
month = Month(m) + 1
// Normalize nsec, sec, min, hour, overflowing into day.
sec, nsec = norm(sec, nsec, 1e9)
min, sec = norm(min, sec, 60)
hour, min = norm(hour, min, 60)
day, hour = norm(day, hour, 24)
// Compute days since the absolute epoch.
// 从公元前292277022399年到当前的天数,为什么是这个年份,网上没找到资料
d := daysSinceEpoch(year)
// Add in days before this month.
d += uint64(daysBefore[month-1])
if isLeap(year) && month >= March {
d++ // February 29
}
// Add in days before today.
d += uint64(day - 1)
// Add in time elapsed today.
abs := d * secondsPerDay
abs += uint64(hour*secondsPerHour + min*secondsPerMinute + sec)
// 转化为1970年到当前时间的秒数
unix := int64(abs) + (absoluteToInternal + internalToUnix)
// Look for zone offset for t, so we can adjust to UTC.
// The lookup function expects UTC, so we pass t in the
// hope that it will not be too close to a zone transition,
// and then adjust if it is.
_, offset, start, end := loc.lookup(unix)
if offset != 0 {
switch utc := unix - int64(offset); {
case utc < start:
_, offset, _, _ = loc.lookup(start - 1)
case utc >= end:
_, offset, _, _ = loc.lookup(end)
}
unix -= int64(offset)
}
// 实际存储的时候,会再转化为从0001-01-01 开始的时间奥
t := unixTime(unix, int32(nsec))
t.setLoc(loc)
return t
}
实际代码比较好懂,总结下时间存储吧
- Now()采用monotonic time 存储结构(因为我们现在还没到2157年),两个Now时间相减的结果是不会出现负数的
- Date()采用wall time(hasMonotonic = 0) 存储,而且存储的值是UTC时区的时间(这个在Date函数的最后部分,详细代码我们在时区节讨论)
- 不论Now还是Date 最终存储的都是0001-01-01到当前时间的秒数,而且是经过了时区调整的(基于UTC时区)
时间创建的两个函数我们分析完了,Sub也有涉猎,基本了解了Time Struct 下面看下时区吧
时区
首先问个问题: 中国标准时间一定是UTC+8么? 答案是否,这个问题我们看完代码就明白了 首先我们得知道一个概念: “时区信息数据库”. 同一个国家或地区,随着时间的推移,政权的更替和国家政策的改变,时区的定义可能会发生改变,比如从非夏令时修改为夏令时,再比如微调时间偏移。 以中国标准时间(时区名:PRC) 为例,我们从时区信息数据库解码了中国标准时间的三个配置 zone={Name:LMT Offset:29143 IsDST:false} zone={Name:CDT Offset:32400 IsDST:true} zone={Name:CST Offset:28800 IsDST:false} 第一个时区,name=LMT,相对于标准时区偏移29143秒,约为8.09小时,非夏令时 第二个时区,name=CDT, 相对于标准时区偏移32400秒,约为9小时,这是中国1986-1991年间实行夏令时间(参见 zh.wikipedia.org/wiki/%E4%B8… 第三个时区,name=CST, 相对于标准时区偏移28800秒,约为8小时,这就是我们常见的UTC+8,东八区了。目前采用这个时区。中国标准时间的三个时区配置,随着时间的变迁,不同时期采用不同的配置,那么哪个时间采用哪个配置,这些时区变更记录也是存储在"时区信息数据库"中。
下面有段代码,可以反编译中国标准时区信息数据,我们来看下结果
package main
import (
"fmt"
"time"
"unsafe"
)
func main() {
// Calling LoadLocation
// Calling LoadLocation
// method with its parameter
locat, err := time.LoadLocation("PRC")
// If error not equal to nil then
// return panic error
if err != nil {
fmt. Printf("loc=nil")
}
fp := unsafe.Pointer(locat)
hh := (*FakeLocation)(fp)
fmt.Printf("\nlen(zone)=%d\n", len(hh.Zone))
for _, item := range hh.Zone {
fmt.Printf("zone=%+v\n", item)
}
fmt. Printf("\nlen(tx)=%d\n", len(hh.Tx))
for _, item := range hh.Tx {
fmt.Printf("Tx=%+v\n", item)
}
// Prints location
fmt.Printf("loc=%+v", *(*FakeLocation)(fp))
}
type FakeLocation struct {
Name string
Zone []FakeZone
Tx []FakeZoneTrans
Extend string
CacheStart int64
CacheEnd int64
CacheZone *FakeZone
}
// A zone represents a single time zone such as CET.
type FakeZone struct {
Name string // abbreviated name, "CET"
Offset int // seconds east of UTC
IsDST bool // is this zone Daylight Savings Time?
}
// A zoneTrans represents a single time zone transition.
type FakeZoneTrans struct {
When int64 // transition time, in seconds since 1970 GMT
Index uint8 // the index of the zone that goes into effect at that time
Isstd, Isutc bool // ignored - no idea what these mean
}
// output
len(zone)=3
zone={Name:LMT Offset:29143 IsDST:false}
zone={Name:CDT Offset:32400 IsDST:true} // 东八区夏令时
zone={Name:CST Offset:28800 IsDST:false} // 东八区非夏令时
len(tx)=28
Tx={When:-576460752303423488 Index:0 Isstd:false Isutc:false}
Tx={When:-2177481943 Index:2 Isstd:false Isutc:false}
Tx={When:-933667200 Index:1 Isstd:false Isutc:false}
Tx={When:-922093200 Index:2 Isstd:false Isutc:false}
Tx={When:-908870400 Index:1 Isstd:false Isutc:false}
Tx={When:-888829200 Index:2 Isstd:false Isutc:false}
Tx={When:-881049600 Index:1 Isstd:false Isutc:false}
Tx={When:-767869200 Index:2 Isstd:false Isutc:false}
Tx={When:-745833600 Index:1 Isstd:false Isutc:false}
Tx={When:-733827600 Index:2 Isstd:false Isutc:false}
Tx={When:-716889600 Index:1 Isstd:false Isutc:false}
Tx={When:-699613200 Index:2 Isstd:false Isutc:false}
Tx={When:-683884800 Index:1 Isstd:false Isutc:false}
Tx={When:-670669200 Index:2 Isstd:false Isutc:false}
Tx={When:-652348800 Index:1 Isstd:false Isutc:false}
Tx={When:-650019600 Index:2 Isstd:false Isutc:false} // 1949-05-27 15:00:00
Tx={When:515527200 Index:1 Isstd:false Isutc:false} // 1986-05-03 18:00:00
Tx={When:527014800 Index:2 Isstd:false Isutc:false}
Tx={When:545162400 Index:1 Isstd:false Isutc:false}
Tx={When:558464400 Index:2 Isstd:false Isutc:false}
Tx={When:577216800 Index:1 Isstd:false Isutc:false}
Tx={When:589914000 Index:2 Isstd:false Isutc:false}
Tx={When:608666400 Index:1 Isstd:false Isutc:false}
Tx={When:621968400 Index:2 Isstd:false Isutc:false}
Tx={When:640116000 Index:1 Isstd:false Isutc:false}
Tx={When:653418000 Index:2 Isstd:false Isutc:false}
Tx={When:671565600 Index:1 Isstd:false Isutc:false} //1991-04-13 18:00:00
Tx={When:684867600 Index:2 Isstd:false Isutc:false} // 1991-09-14 17:00:00
Tx 就是时区变更的记录了,它是按照时间升序排列的。看下Tx 各个字段的意思
- When: 变更时间(utc 时间戳)
- Index: zone 列表的索引位置
- Isstd: 感觉不重要
- Isutc: 字面理解即可 比如: Tx={When:-650019600 Index:2 Isstd:false Isutc:false} // 1949-05-27 15:00:00 的意思就是从1949-05-27 15:00:00(utc) 开始采用zone[2]配置,一直到下个Tx的When
到这里我们基本理解时区了吧,下面看下实现吧
type Location struct {
name string // 时区名:中国标准时间:name=PRC
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
}
type zone struct {
name string // abbreviated name, "CET"
offset int // 时间偏移,比如东八区是:28800(单位秒)
isDST bool // is this zone Daylight Savings Time?
}
// A zoneTrans represents a single time zone transition.
type zoneTrans struct {
when int64 // 时区变更开始时间
index uint8 // 变更后采用的时区index,index即zone数组的下标
isstd, isutc bool // ignored - no idea what these mean
}
有了上面的背景知识,再看Location 我们就好理解了,再看下核心的lookup函数
func (l *Location) lookup(sec int64) (name string, offset int, start, end int64) {
l = l.get()
if len(l.zone) == 0 {
name = "UTC"
offset = 0
start = alpha
end = omega
return
}
if zone := l.cacheZone; zone != nil && l.cacheStart <= sec && sec < l.cacheEnd {
name = zone.name
offset = zone.offset
start = l.cacheStart
end = l.cacheEnd
return
}
if len(l.tx) == 0 || sec < l.tx[0].when { // 特殊情况,找到index=0的那个tx对应zone
zone := &l.zone[l.lookupFirstZone()]
name = zone.name
offset = zone.offset
start = alpha
if len(l.tx) > 0 {
end = l.tx[0].when
} else {
end = omega
}
return
}
// Binary search for entry with largest time <= sec.
// Not using sort.Search to avoid dependencies.
tx := l.tx
end = omega
lo := 0
hi := len(tx)
for hi-lo > 1 { // tx 是有序的,所以使用二分查找
m := lo + (hi-lo)/2
lim := tx[m].when
if sec < lim {
end = lim
hi = m
} else {
lo = m
}
}
zone := &l.zone[tx[lo].index]
name = zone.name
offset = zone.offset
start = tx[lo].when
// end = maintained during the search
// If we're at the end of the known zone transitions,
// try the extend string.
if lo == len(tx)-1 && l.extend != "" {
if ename, eoffset, estart, eend, ok := tzset(l.extend, end, sec); ok {
return ename, eoffset, estart, eend
}
}
return
}
通过二分查找,找到当前时间所属的tx区间,然后取zone[tx.Index]作为时区配置,很简单吧。
了解了时区相关,然后我们再返回time 看他们里面的逻辑就比较简单了吧,不赘述。 下周分析Timer源码吧。
golang社区
知识星球,一起golang: t.zsxq.com/nUR723R