4.3 Time 类型详解

    程序中应使用 Time 类型值来保存和传递时间,而不是指针。就是说,表示时间的变量和字段,应为 time.Time 类型,而不是 *time.Time. 类型。一个 Time 类型值可以被多个 go 协程同时使用。时间点可以使用 Before、After 和 Equal 方法进行比较。Sub 方法让两个时间点相减,生成一个 Duration 类型值(代表时间段)。Add 方法给一个时间点加上一个时间段,生成一个新的 Time 类型时间点。

    Time 零值代表时间点 January 1, year 1, 00:00:00.000000000 UTC。因为本时间点一般不会出现在使用中,IsZero 方法提供了检验时间是否是显式初始化的一个简单途径。

    每一个 Time 都具有一个地点信息(即对应地点的时区信息),当计算时间的表示格式时,如 Format、Hour 和 Year 等方法,都会考虑该信息。Local、UTC 和 In 方法返回一个指定时区(但指向同一时间点)的 Time。修改地点 / 时区信息只是会改变其表示;不会修改被表示的时间点,因此也不会影响其计算。

    通过 == 比较 Time 时,Location 信息也会参与比较,因此 Time 不应该作为 map 的 key。

    要讲解 time.Time 的内部结构,得先看 time.Now() 函数。

    1. // Now returns the current local time.
    2. func Now() Time {
    3. sec, nsec := now()
    4. return Time{sec + unixToInternal, nsec, Local}
    5. }

    now() 的具体实现在 runtime 包中,以 linux/amd64 为例,在 sys_linux_amd64.s 中的 time · now,这是汇编实现的:

    • 如果 clock_gettime 不存在,则使用精度差些的系统调用 gettimeofday。得到的是 struct timeval 类型。(最多到微秒)

    注意: 这里使用了 Linux 的 vdso 特性,不了解的,可以查阅相关知识。

    虽然 timespectimeval 不一样,但结构类似。因为 now() 函数返回两个值:sec( 秒 ) 和 nsec( 纳秒 ),所以,time · now 的实现将这两个结构转为需要的返回值。需要注意的是,Linux 系统调用返回的 sec( 秒 ) 是 Unix 时间戳,也就是从 1970-1-1 算起的。

    回到 time.Now() 的实现,现在我们得到了 sec 和 nsec,从 Time{sec + unixToInternal, nsec, Local} 这句可以看出,Time 结构的 sec 并非 Unix 时间戳,实际上,加上的 unixToInternal 是 1-1-1 到 1970-1-1 经历的秒数。也就是 Time 中的 sec 是从 1-1-1 算起的秒数,而不是 Unix 时间戳。

    常用函数或方法

    Time 相关的函数和方法较多,有些很容易理解,不赘述,查文档即可。

    因为 Time 的零值是 sec 和 nsec 都是 0,表示 1 年 1 月 1 日。

    Time.IsZero() 函数用于判断 Time 表示的时间是否是 0 值。

    相关函数或方法:

    • time.Unix(sec, nsec int64) 通过 Unix 时间戳生成 time.Time 实例;
    • time.Time.Unix() 得到 Unix 时间戳;
    • time.Time.UnixNano() 得到 Unix 时间戳的纳秒表示;

    这是实际开发中常用到的。

    • time.Parse 和 time.ParseInLocation
    • time.Time.Format

    解析

    对于解析,要特别注意时区问题,否则很容易出 bug。比如:

    2016-06-13 09:14:00 这个时间可能是参数传递过来的。这段代码的结果跟预期的不一样。

    原因是 time.Now() 的时区是 time.Local,而 解析出来的时区却是 time.UTC(可以通过 Time.Location() 函数知道是哪个时区)。在中国,它们相差 8 小时。

    所以,一般的,我们应该总是使用 time.ParseInLocation 来解析时间,并给第三个参数传递 time.Local

    为什么是 2006-01-02 15:04:05

    • 官方说,使用具体的时间,比使用 Y-m-d H:i:s 更容易理解和记忆;这么一说还真是 ~
    • 而选择这个时间点,也是出于好记的考虑,官方的例子:Mon Jan 2 15:04:05 MST 2006,另一种形式 01/02 03:04:05PM '06 -0700,对应是 1、2、3、4、5、6、7;常见的格式:2006-01-02 15:04:05,很好记:2006 年 1 月 2 日 3 点 4 分 5 秒 ~

    如果你是 PHPer,喜欢 PHP 的格式,可以试试 times 这个包。

    格式化

    时间格式化输出,使用 Format 方法,layout 参数和 Parse 的一样。Time.String() 方法使用了 2006-01-02 15:04:05.999999999 -0700 MST 这种 layout

    Time 实现了 encoding 包中的 BinaryMarshalerBinaryUnmarshalerTextMarshalerTextUnmarshaler 接口;encoding/json 包中的 MarshalerUnmarshaler 接口。

    它还实现了 gob 包中的 GobEncoderGobDecoder 接口。

    对于文本序列化 / 反序列化,通过 Parse 和 实现;对于二进制序列化,需要单独实现。

    对于 json,使用的是 time.RFC3339Nano 这种格式。通常程序中不使用这种格式。解决办法是定义自己的类型。如:

    1. type OftenTime time.Time
    2. func (self OftenTime) MarshalJSON() ([]byte, error) {
    3. t := time.Time(self)
    4. if y := t.Year(); y < 0 || y >= 10000 {
    5. return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
    6. }
    7. // 注意 `"2006-01-02 15:04:05"`。因为是 JSON,双引号不能少
    8. }

    比如,有这么个需求:获取当前时间整点的 Time 实例。例如,当前时间是 15:54:23,需要的是 15:00:00。我们可以这么做:

    实际上,time 包给我们提供了专门的方法,功能更强大,性能也更好,这就是 RoundTrunate,它们区别,一个是取最接近的,一个是向下取整。

    使用示例:

    1. t, _ := time.ParseInLocation("2006-01-02 15:04:05", "2016-06-13 15:34:39", time.Local)
    2. // 整点(向下取整)
    3. fmt.Println(t.Truncate(1 * time.Hour))
    4. // 整点(最接近)
    5. fmt.Println(t.Round(1 * time.Hour))
    6. // 整分(向下取整)
    7. fmt.Println(t.Truncate(1 * time.Minute))
    8. // 整分(最接近)
    9. fmt.Println(t.Round(1 * time.Minute))
    10. t2, _ := time.ParseInLocation("2006-01-02 15:04:05", t.Format("2006-01-02 15:00:00"), time.Local)
    11. fmt.Println(t2)

    导航