Golang Time 包源码分析1-Time & 时区类实现

3,238 阅读12分钟

一. 准备知识

在分析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 time 存储格式-1 上图是存储图。hasMonotonic标志位存储在wall字段的最高位,为1,wall的64~31位存储从1985-01-01 00:00:00到当前时间的秒数,wall的1-30位存储当前时间的纳秒数。ext字段存储:进程启动到当前时间点的纳秒数.不同的系统获取该值的方式不同,它是系统的调用的返回值. 这个值就是monotonic time,独立于wall time,单调递增。

第二种情况: hasMonotonic = 0 time 存储格式-2

上图是存储图。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

博客: blog.17ledong.com/