time.Time

作者: HackerZGZ | 来源:发表于2018-08-15 00:53 被阅读0次

    time 作为使用频次非常高的包, Go Team 是如何实现这个包的呢?里面有多少可以挖掘的小技巧呢?没错,由于自 1.9 后,Go Team 更新了 time 包的实现,其中主要变更部分更是由 Russ Cox 操刀,那么我们今天就来源码解析下它。

    注意:本文的源码解析基于 1.10.2 版本。

    Time

    我们都知道 time 中有三个比较常用的数据结构,分别为:

    • time.Time
    • time.Duration
    • time.C

    而下面我们来逐一解析它们的作用。

    最常见的情况下,我们会通过 time.Now() 函数得到一个 time.Time 类型的变量,这个变量代表着当前的时间,但是有多少人知道 time.Time 类型里面的字段都代表着什么含义呢?

    在讲解 time.Time 结构之前,我们需要知道两个概念:

    • wall time
    • monotonic time

    如果你已经知道这两个时间的概念,那么可以跳过这一段。

    wall time 顾名思义,就是挂在墙上的时钟,我们在计算机中能看到的当前时间就是 wall time ,但是这个时间是可以通过 人为设置 或者 NTP服务同步 被修改,常见的场景就是通过修改时间延长收费软件的试用期。

    monotonic time 是一个单调递增的时间,当操作系统被初始化时, jiffies 变量被初始化为 0 ,每当接收到一个 timer interrupt ,则 jiffies 自增 1 ,所以它必然是一个不可修改的单调递增时间。

    所以,在操作系统中如果需要 显示 时间的时候,会使用 wall time ,而需要 测量 时间的时候,会使用 monotonic time

    但是为了避免拆分 API, time 包将这两个时间合并在 time.Time 这个结构中,当需要读取时间的时候会使用 wall time ,如果需要测量时间就会使用 monotonic time。

    下面简单举例使用两种不同时间的函数,但需要注意的时候,如果两个时间同时存在 monotonic time ,则会忽略 wall time ,反之才会使用 wall time:

    • wall time
      • time.Since(start)
      • time.Until(deadline)
      • time.Now().Before(deadline)
    • monotonic time
      • t.AddDate(y, m, d)
      • t.Round(d)
      • t.Truncate(d)

    多说无益,我们来看下 time.Time 是怎么同时存储 wall time 以及 monotonic time 的。

    // src/time/time.go
    
    // 1. Time 能够代表纳秒精度的时间
    // 2. 因为 Time 并非并发安全,所以在存储或者是传递的时候,都应该使用值引用。
    // 3. 在 Go 中, == 运算符不仅仅会比较时刻,还会比较 Location 以及单调时钟,
    //    因此在不保证所有时间设置为相同的位置的时候,不应将 time.Time 
    //    作为 map 或者 database 的键。如果必须要使用,应先通过 UTC 或者 Local 
    //    方法将单调时钟剥离。
    //
    type Time struct {
        // wall 和 ext 字段共同组成 wall time 秒级、纳秒级,monotonic time 纳秒级
        // 的时间精度,先看下 wall 这个 无符号64位整数 的结构。
        //
        //          +------------------+--------------+--------------------+
        // wall =>  | 1 (hasMonotonic) | 33 (second)  |  30 (nanosecond)   |
        //          +------------------+--------------+--------------------+
        // 
        // 所以 wall 字段会有两种情况,分别为
        // 1. 当 wall 字段的 hasMonotonic 为 0 时,second 位也全部为 0,ext 字段会存储
        //    从 1-1-1 开始的秒级精度时间作为 wall time 。
        // 2. 当 wall 字段的 hasMonotonic 为 1 时,second 位会存储从 1885-1-1 开始的秒
        //    级精度时间作为 wall time,并且 ext 字段会存储从操作系统启动后的纳秒级精度时间
        //    作为 monotonic time 。
        wall uint64
        ext  int64
        
        // Location 作为当前时间的时区,可用于确定时间是否处在正确的位置上。
        // 当 loc 为 nil 时,则表示为 UTC 时间。
        // 因为北京时区为东八区,比 UTC 时间要领先 8 个小时,
        // 所以我们获取到的时间默认会记为 +0800
        loc *Location
    }
    

    为了加深理解 time.Time 的存储机制,我们通过以下的方法进行解析。

    // src/time/time.go
    
    const (
        // 代表无符号64位整数的首位
        hasMonotonic = 1 << 63
        
        // maxWall 和 minWall 是指 hasMonotonic 为 1 的情况下,
        // wall time 的最大以及最小的时间范围。
        maxWall      = wallToInternal + (1<<33 - 1) // year 2157
        minWall      = wallToInternal               // year 1885
        
        // 纳秒位位置的辅助常量
        nsecMask     = 1<<30 - 1
        nsecShift    = 30
    )
    
    // 注意:以下方法均为包内辅助方法,会通过指针接收器进行操作,减轻调用负担,
    // 但我们在使用 time.Time 时应该尽量避免使用指针,以免出现竞态争用。
    
    // sec 返回时间的秒数
    func (t *Time) sec() int64 {
        if t.wall&hasMonotonic != 0 {
            // hasMonotonic 为 1,则 second 位记录从 1885-1-1 开始的秒数
            // 则返回值为以下两个值相加:
            // wallToInternal = 1885-1-1 00:00:00 的秒数 = 59453308800
            // t.wall<<1>>(nsecShift+1) = 1885-1-1 00:00:00 到现在的秒数
            return wallToInternal + int(t.wall<<1>>(nsecShift+1))
        }
        return int64(t.ext)  // hasMonotonic 为 0 ,返回 ext 为 wall time 秒数
    }
    
    // addSec 在当前时间基础上加上 d 秒
    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 { // 判断 wall 的 second 不会溢出
                t.wall = t.wall&nsecMask | uint64(dsec)<<nsecShift | hasMonotonic
                return
            }
            // second 位已经不足以存下了 wall time 的秒数,需要去掉单调时钟,并
            // 其移动到 ext 字段中,移动完成后,执行下面的 t.ext += d 语句即可
            t.stripMono()
        }
        
        // 如果 hasMonotonic 为 0,直接就在 ext 字段上面添加就好了
        t.ext += d
    }
    
    // stripMono 去除单调时钟
    func (t *Time) stripMono() {
        if t.wall&hasMonotonic != 0 {
            t.ext = t.sec()
            t.wall &= nsecMask
        }
    }
    

    以上的这些方法都是 time 包中的匿名方法,接下来我们看看平常能使用到的 After / Before / Equal 等方法。

    // src/time/time.go
    
    // 判断 t 时间是否晚于 u 时间
    func (t Time) After(u Time) bool {
        if t.wall&u.wall&hasMonotonic != 0 { // 判断 t 和 u 是否都有单调时钟
            return t.ext > u.ext             // 只需判断单调时钟即可
        }
        ts := t.sec()    // 否则需要从 wall 字段中获取秒数
        us := u.sec()
        // 判断 t 的秒数是否大于 u
        // 如果秒数相同,则比较纳秒数
        return ts > us || ts == us && t.nsec() > u.nsec() 
    }
    
    // 判断 t 时间是否早于 u 时间
    func (t Time) Before(u Time) bool {
        if t.wall&u.wall&hasMonotonic != 0 { // 同上
            return t.ext < u.ext
        }
        // 同上反之
        return t.sec() < u.sec() || t.sec() == u.sec() && t.nsec() < u.nsec()
    }
    
    // 判断 t 与 u 时间是否相同,不判断 location
    // 如 6:00 +0200 CEST  与  4:00 UTC  会返回 true
    // 如需同时判断 location ,可以使用 == 操作符
    func (t Time) Equal(u Time) bool {
        if t.wall&u.wall&hasMonotonic != 0 { // 同上
            return t.ext == u.ext
        }
        // 判断 t 与 u 的秒数以及纳秒数是否相同
        return t.sec() == u.sec() && t.nsec() == u.nsec()
    }
    

    这里其实可以将 Before 函数实现为: return !t.After(u) 是否会更清晰些?而且从代码实现细节上看 BeforeAfter 上有细微的差别,从 git blame 看也是同一人所写,不清楚是为什么,如有知道此处细节的同学可以留言解释下,谢谢。

    其实从上面 3 个函数可以看出来,如果两个时间都是带有单调时钟的时候,处理时候会更高效,那么我们怎么才能将获取带有单调函数的时间呢?

    Now

    其实我们从 time 包中获取时间最常用的函数就是 time.Now() 了,该函数返回机器的当前时间,我们来看看具体是如何实现的?

    // src/time/time.go
    
    const (
        // unix 时间戳为从 1970-01-01 00:00:00 开始到当前的秒数
        // time 包的 internal 时间为从 0000-00-00 00:00:00 开始的秒数
        //
        // 以下两个常量用于在 internal 与 unix 时间戳之间转换的辅助常量
        unixToInternal int64 = (1969*365 + 1969/4 - 1969/100 + 1969/400) * secondsPerDay
        internalToUnix int64 = -unixToInternal
    
        // minWall = wallToInternal
        wallToInternal int64 = (1884*365 + 1884/4 - 1884/100 + 1884/400) * secondsPerDay
        internalToWall int64 = -wallToInternal
    )
    
    // Provided by package runtime.
    func now() (sec int64, nsec int32, mono uint64)
    // darwin,amd64 darwin,386 windows => src/runtime/timeasm.go
    // other                           => src/runtime/timestub.go
    
    // 返回当前本机时间
    func Now() Time {
        sec, nsec, mono := now()
        // 计算从 1885-1-1 开始到现在的秒数
        // unixToInternal = 1970-01-01 00:00:00
        // minWall        = 1885-01-01 00:00:00
        sec += unixToInternal - minWall
        if uint64(sec)>>33 != 0 { // 如果有溢出,则不能用 wall 的 second 保存完整的时间戳
            // 返回自 1970-01-01 00:00:00 开始的秒数
            return Time{uint64(nsec), sec + minWall, Local}
        }
        return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
    }
    

    我们可以看到,Now 函数主要依赖的是私有函数 now 返回的三个变量 second / nsec / mono ,而 now 函数的实现则交由 runtime 实现,因为其实现为汇编,不在本文探讨范围内,读者只需要知道该函数会返回本机的当前 纳秒 以及 单调时钟 即可,如有兴趣的读者可以留言,我会另写一文解析 now 的汇编实现。

    此外,我们还可以通过如下函数获取 time.Time ,但因为它们具体实现基本一致,所以我们只挑其中一个解析。

    • time.Unix(sec, nsec)
    • time.Date(year, month, day, hour, min, sec, nsec, loc)
    • time.ParseInLocation(layout, value, loc)

    time.Unix(sec, nsec) 函数通过传入 unix timestamp 获取 time.Time 结构,默认返回的是 UTC 时区。

    // src/time/time.go
    
    // 通过传入自 1970-01-01 00:00:00 开始的参数生成对应时间点的变量
    func Unix(sec int64, nsec int64) Time {
        // 如果 nsec 不处于 [0, 999999999] 的闭区间
        if nsec < 0 || nsec > 1e9 {
            // 将超出 nsec 正常精度的数值转移到 sec 中
            n := nsec / 1e9
            sec += n
            nsec -= n * 1e9
            if nsec < 0 {
                nsec += 1e9
                sec--
            }
        }
        return unixTime(sec, int32(nsec))
    }
    
    func unixTime(sec int64, nsec int32) Time {
        // 模拟没有单调时钟的时间结构
        // wall => uint64(nsec)
        // ext  => sec+unixToInternal 
        return Time{uint64(nsec), sec+unixToInternal, Local}
    }
    

    通过以上解析的函数实现可知,如果想获取带有 单调时钟 的时间只能通过 time.Now() 获取,而由于 wall 的 second 有 33 位,所以只要我们在 2157-01-01 00:00:00 UTC 前调用 time.Now() 获取到的时间都是带有 单调时钟 的,所以放心使用吧~

    相关文章

      网友评论

          本文标题:time.Time

          本文链接:https://www.haomeiwen.com/subject/bfxibftx.html