time.Time

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() 获取到的时间都是带有 单调时钟 的,所以放心使用吧~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容