关于golang的time包总结

前言

各种编程语言都少不了与时间有关的操作,因为很多判断都是基于时间,因此正确和方便的使用时间库就很重要额。
golang提供了import "time"包用来处理时间相关操作,找到合适的api可以高效的处理时间,找到正确的使用方式可以少出bug。
可以去百度2020 年的第一天,程序员鸭血粉丝又碰上生产事故,就是没有正确理解Java关于时间的处理产生的bug,貌似不少人中招啊。

time包详解

可以去【点击跳转】这个网址查看并学习time包吧。
我下面列出一些我常用的时间操作吧。

package main

import (
    "archive/zip"
    "errors"
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
    "time"

    // go1.5增加功能,将时区文件嵌入程序,官方说可执行程序会增大800K
    // 为了兼容老代码,可以在编译时加入"-tags timetzdata"就可以不用导入下面的包
    _ "time/tzdata"
)

func main() {
    err := testZone()
    if err != nil {
        panic(err)
    }

    err = testDateTime()
    if err != nil {
        panic(err)
    }

    err = testTimer()
    if err != nil {
        panic(err)
    }

    err = testTick()
    if err != nil {
        panic(err)
    }
}

func testTick() error {
    fmt.Println("testTick:")

    i := 0
    // 按照如下方式定时获取time非常简洁
    for t := range time.Tick(time.Second) {
        i++
        if i > 3 {
            break
        }
        fmt.Println(t.String())
    }

    // 如下测试是我的常规用法
    t := time.NewTicker(time.Second)
    send := make(chan int)
    go func(c chan<- int) {
        i := 0
        for {
            time.Sleep(time.Millisecond * 600)
            c <- i
            i++
        }
    }(send)
    go func(c <-chan int) {
        for {
            select {
            case tmp, ok := <-t.C:
                fmt.Println(tmp.String(), ok)
            case tmp, ok := <-c:
                fmt.Println(tmp, ok)
            }
        }
    }(send)
    time.Sleep(time.Second * 10)

    t.Reset(time.Second)
    t.Stop() // 这两个方法很好理解,就不细讲了
    return nil
}

func testTimer() error {
    fmt.Println("testTimer:")
    // 大多数场景用下面这种方式处理超时逻辑
    // FIXME: 特别注意下面方法存在内存泄露,当大量调用chanTimeout
    // FIXME: 会产生大量time.After,此时如果都在超时时间内走handle
    // FIXME: 那么time.After产生的对象都占着内存,直到超过timeout才会GC释放
    chanTimeout := func(c <-chan int, timeout time.Duration) {
        select {
        case tmp, ok := <-c:
            // handle(tmp, ok)
            fmt.Println(tmp, ok)
        case <-time.After(timeout):
            fmt.Println("timeout")
        }
    }
    // FIXME: 使用下面方法更安全,当在超时时间内走到处理流程,手动释放内存
    chanTimeout = func(c <-chan int, timeout time.Duration) {
        t := time.NewTimer(timeout)
        select {
        case tmp, ok := <-c:
            t.Stop() // 当走正常逻辑时手动停掉timer
            // handle(t, ok)
            fmt.Println(tmp, ok)
        case <-t.C:
            fmt.Println("timeout")
        }
    }

    send := make(chan int)
    go chanTimeout(send, time.Second)
    time.Sleep(time.Millisecond * 800)
    select {
    case send <- 100: // 在timeout之前进程处理逻辑
    default:
    }

    go chanTimeout(send, time.Second)
    time.Sleep(time.Second * 2)
    select { // 可以尝试不用select + default,只简单的使用send <- 200会不会报错
    case send <- 200: // 直接进入timeout逻辑
    default:
    }

    fmt.Println(time.Now().String())
    timer := time.AfterFunc(time.Second, func() {
        fmt.Println(time.Now().String())
    })
    time.Sleep(time.Second * 2)
    timer.Reset(time.Second * 5) // 重置一下,5秒后会再打印一条
    time.Sleep(time.Second * 6)
    select {
    case <-timer.C:
    default:
    }
    return nil
}
func testDateTime() error {
    fmt.Println("testDateTime:")
    now := time.Now()
    /*
       Date返回一个时区为loc、当地时间为:year-month-day hour:min:sec + nsec
       month、day、hour、min、sec和nsec的值可能会超出它们的正常范围,在转换前函数会自动将之规范化。
       如October 32被修正为November 1。
       夏时制的时区切换会跳过或重复时间。如,在美国,March 13, 2011 2:15am从来不会出现,而November 6, 2011 1:15am 会出现两次。此时,时区的选择和时间是没有良好定义的。Date会返回在时区切换的两个时区其中一个时区
       正确的时间,但本函数不会保证在哪一个时区正确。
       如果loc为nil会panic。
    */
    // 下面同样展示了单独获取 年-月-日- 时:分:秒:纳秒 时区的方法
    setTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), now.Location())
    fmt.Println(setTime.String())

    y, mo, d := setTime.Date()  // 批量获取 年-月-日
    h, mi, s := setTime.Clock() // 批量获取 时:分:秒
    fmt.Println(y, mo, d, h, mi, s)

    fmt.Println("时间戳,精确到秒数:", setTime.Unix())
    fmt.Println("时间戳,精确到纳秒数:", setTime.UnixNano())
    fmt.Println("获取当前时间的UTC时间对象:", setTime.UTC())
    fmt.Println("获取当前时间的Local时间对象:", setTime.Local())
    fmt.Println("今天是本年的第几天:", setTime.YearDay())
    fmt.Println("当前星期:", setTime.Weekday().String())

    setTime = time.Date(2020, 1, 1, 0, 0, 0, 0, now.Location())
    fmt.Println(setTime.ISOWeek()) // 正常的2020年的第1周
    setTime = time.Date(2021, 1, 1, 0, 0, 0, 0, now.Location())
    fmt.Println(setTime.ISOWeek()) // 会计算到上一年的2020年的第53
    setTime = time.Date(2019, 12, 31, 0, 0, 0, 0, now.Location())
    fmt.Println(setTime.ISOWeek()) // 会计算到下一年的2020年的第1周

    // 将时间戳转换为时间对象,会使用当前默认的时区time.Local,可以使用In方法指定时区
    // 第一个参数为时间戳,精确到秒,下面表示在当前秒数增加1000纳秒,及增加1毫秒
    stampTime := time.Unix(setTime.Unix(), 1000).In(now.Location())
    fmt.Println(stampTime.String())
    // 第二个参数精确到纳秒,下面表示在当前纳秒数增加60秒,及增加1分钟
    stampTime = time.Unix(60, setTime.UnixNano()).In(now.Location())
    fmt.Println(stampTime.String())

    // 使用Add可以增加时间,传入负数就减去时间
    fmt.Printf("时间增加1小时:%v,时间减小1小时:%v\n", now.Add(time.Hour), now.Add(-time.Hour))
    // 使用AddDate可以增加年月日,传入负数减去日期
    fmt.Printf("时间增加1年1月1日:%v,时间减小1年1月1日:%v\n", now.AddDate(1, 1, 1), now.AddDate(-1, -1, -1))
    // 可以混合使用,计算任意前后时间,特别注意该方法会自动识别平年闰年,以及月份等各种因素
    fmt.Printf("时间增加1年1月1日1分钟:%v\n", now.AddDate(1, 2, 3).Add(time.Minute))

    const timeFormat = "2006-01-02 15:04:05"
    // 可以使用库里面的"time.ANSIC"这种格式,但我一般习惯上面的"timeFormat"的格式
    // go格式化字符串使用特定的数字和字符,这些数字和字符都是互斥的,只要格式字符串出现就能正确解析
    nowStr := now.Format(timeFormat)
    fmt.Println("时间转换为字符串 Format:", nowStr)

    buf := make([]byte, 0, 64)
    // 该方法为基本格式化时间对象为字符串,用该方法可以避免申请临时对象
    // 需要由用户控制传入buf的长度,如果长度不够,则返回值是扩容后的可用的数据,而bug里面数据无效
    fmt.Printf("时间转换为字符串 AppendFormat:[%s]\n", now.AppendFormat(buf, timeFormat))

    // 下面将时间字符串转换为时间对象,如果缺少表示时区的信息,Parse会将时区设置为UTC
    // 如果字符串中出现时区相关字符串则会使用字符串里面转换出来的时区
    // 如果只有时区偏移,则结果时区对象只有偏移
    setTime, err := time.Parse(timeFormat, nowStr)
    if err != nil {
        return err
    }

    // 下面将时间字符串转换为时间对象,需要传入时区对象
    // 如果格式化字符串中带时区字符,那么后面传入的时区对象无效
    setTime, err = time.ParseInLocation(timeFormat, nowStr, now.Location())
    if err != nil {
        return err
    }

    // 下面这两个方法计算时间间隔,返回间隔秒数
    // Since返回从t到现在经过的时间,等价于time.Now().Sub(t)
    fmt.Println("time.Since:", time.Since(stampTime))
    // Until返回从现在到t经过的时间,等价于t.Sub(time.Now())
    fmt.Println("time.Until:", time.Until(stampTime))

    // 将时间字符串转换为 time.Duration 对象
    // 字符串可以使用这些单位: "ns", "us" (or "µs"), "ms", "s", "m", "h"
    // 主要用于简写时间,而不必记住一长串数字
    fmt.Print("time.ParseDuration: ")
    fmt.Println(time.ParseDuration("24h5m6s123ms456us321ns"))

    // now 在 stampTime 之后则返回true,否则返回false
    fmt.Println("now.After: ", now.After(stampTime))
    // now 在 stampTime 之前则返回true,否则返回false
    fmt.Println("now.Before: ", now.Before(stampTime))

    // 判断两个时间相等则返回true
    fmt.Println("now.Equal:", now.Equal(stampTime))

    // 将时间对象序列化为字节数组,内部调用now.MarshalBinary()
    // 下面会演示序列化会将时区信息带上
    now = now.In(time.FixedZone("janbar", 5*3600))
    buf, err = now.GobEncode()
    if err != nil {
        return err
    }
    fmt.Printf("[% x]\n", buf)

    // 重置now,从buf中反序列化得到时间对象,最终now为buf反序列化的时间对象
    // 内部调用now.UnmarshalBinary(data)
    now = time.Unix(1, 1)
    err = now.GobDecode(buf)
    if err != nil {
        return err
    }
    // 结果有带上时区偏移,序列化最大的好处就是在各种传递时方便些
    fmt.Println("now.GobDecode:", now.String())

    // 判断时间为0则为true
    fmt.Println("now.IsZero:", now.IsZero())

    // 将时间对象序列化为json字符串,感觉实际上就是加了前后两个双引号
    buf, err = now.MarshalJSON()
    if err != nil {
        return err
    }
    fmt.Printf("[%s]\n", buf)
    // 反序列化
    now = time.Unix(1, 1)
    err = now.UnmarshalJSON(buf)
    if err != nil {
        return err
    }
    fmt.Println("now.UnmarshalJSON:", now.String())

    // 将时间对象序列化为text字符串
    buf, err = now.MarshalText()
    if err != nil {
        return err
    }
    fmt.Printf("[%s]\n", buf)
    // 反序列化
    now = time.Unix(1, 1)
    err = now.UnmarshalText(buf)
    if err != nil {
        return err
    }
    fmt.Println("now.UnmarshalText:", now.String())

    // 测试t.Round方法,下面是go源码中的测试用例
    // 测试t.Round方法,下面是go源码中的测试用例,可以看源码与t.Round有所区别
    t := time.Date(2012, 12, 7, 12, 15, 30, 918273645, time.Local)
    testDuration := []time.Duration{
        time.Nanosecond,
        time.Microsecond,
        time.Millisecond,
        time.Second,
        2 * time.Second,
        time.Minute,
        10 * time.Minute,
        time.Hour,
    }
    for _, d := range testDuration {
        fmt.Printf("t.Round   (%6s) = %s\n", d, t.Round(d).Format(time.RFC3339Nano))
        fmt.Printf("t.Truncate(%6s) = %s\n", d, t.Truncate(d).Format(time.RFC3339Nano))
    }
    return nil
}

func testZone() error {
    const zone = "Asia/Shanghai"
    now := time.Now()
    /*
       LoadLocation返回使用给定的名字创建的Location。
       如果name是""或"UTC",返回UTC;如果name是"Local",返回Local,
       否则name应该是IANA时区数据库里有记录的地点名(该数据库记录了地点和对应的时区),如"America/New_York"。
       LoadLocation函数需要的时区数据库可能不是所有系统都提供,特别是非Unix系统。
       此时LoadLocation会查找环境变量ZONEINFO指定目录或解压该变量指定的zip文件(如果有该环境变量).
       然后查找Unix系统的惯例时区数据安装位置,最后查找$GOROOT/lib/time/zoneinfo.zip。

       注意:如果没有上面的(import _ "time/tzdata"),调用如下方法在没有安装时区文件的系统上会报错。
         例如在一些精简的docker镜像里面,以及那些没安装并配置go环境变量的电脑。
         (安装后可以通过$GOROOT/lib/time/zoneinfo.zip得到时区信息)。
    */
    toc0, err := time.LoadLocation(zone)
    if err != nil {
        return err
    }

    /*
       LoadLocationFromTZData返回一个具有给定名称的位置。
       从IANA时区数据库格式的数据初始化,数据应采用标准IANA时区文件的格式。
       (例如,Unix系统上/etc/localtime的内容)。

       注意:如果想不通过(import _ "time/tzdata")方式加载时区文件,毕竟能精简也是极好的。
            可以提取出需要的时区文件,手动转换为数据通过如下方式也可以得到时区对象。
            甚至可以将读取出来的数据做成字节数组,嵌入代码,只嵌入我们需要的时区文件即可。
    */
    data, err := getZoneInfoData(zone)
    if err != nil {
        return err
    }
    toc1, err := time.LoadLocationFromTZData(zone, data)
    if err != nil {
        return err
    }
    // 使用该方法将数据制作成代码
    fmt.Printf("[%s]\n", byteToString(data))
    /*
       将数据嵌入代码,此时可以针对需要的时区文件嵌入代码,有兴趣的可以把下面数据压缩一下。
    */
    dataZone := []byte{0x54, 0x5a, 0x69, 0x66, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0c, 0x80, 0x00, 0x00, 0x00, 0xa0,
        0x97, 0xa2, 0x80, 0xa1, 0x79, 0x04, 0xf0, 0xc8, 0x59, 0x5e, 0x80, 0xc9, 0x09, 0xf9, 0x70, 0xc9, 0xd3,
        0xbd, 0x00, 0xcb, 0x05, 0x8a, 0xf0, 0xcb, 0x7c, 0x40, 0x00, 0xd2, 0x3b, 0x3e, 0xf0, 0xd3, 0x8b, 0x7b,
        0x80, 0xd4, 0x42, 0xad, 0xf0, 0xd5, 0x45, 0x22, 0x00, 0xd6, 0x4c, 0xbf, 0xf0, 0xd7, 0x3c, 0xbf, 0x00,
        0xd8, 0x06, 0x66, 0x70, 0xd9, 0x1d, 0xf2, 0x80, 0xd9, 0x41, 0x7c, 0xf0, 0x1e, 0xba, 0x52, 0x20, 0x1f,
        0x69, 0x9b, 0x90, 0x20, 0x7e, 0x84, 0xa0, 0x21, 0x49, 0x7d, 0x90, 0x22, 0x67, 0xa1, 0x20, 0x23, 0x29,
        0x5f, 0x90, 0x24, 0x47, 0x83, 0x20, 0x25, 0x12, 0x7c, 0x10, 0x26, 0x27, 0x65, 0x20, 0x26, 0xf2, 0x5e,
        0x10, 0x28, 0x07, 0x47, 0x20, 0x28, 0xd2, 0x40, 0x10, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01,
        0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02,
        0x01, 0x02, 0x01, 0x02, 0x00, 0x00, 0x71, 0xd7, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x90, 0x01, 0x04, 0x00,
        0x00, 0x70, 0x80, 0x00, 0x08, 0x4c, 0x4d, 0x54, 0x00, 0x43, 0x44, 0x54, 0x00, 0x43, 0x53, 0x54, 0x00,
        0x54, 0x5a, 0x69, 0x66, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x1d, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0c, 0xff, 0xff, 0xff, 0xff, 0x7e, 0x36, 0x43,
        0x29, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x97, 0xa2, 0x80, 0xff, 0xff, 0xff, 0xff, 0xa1, 0x79, 0x04, 0xf0,
        0xff, 0xff, 0xff, 0xff, 0xc8, 0x59, 0x5e, 0x80, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x09, 0xf9, 0x70, 0xff,
        0xff, 0xff, 0xff, 0xc9, 0xd3, 0xbd, 0x00, 0xff, 0xff, 0xff, 0xff, 0xcb, 0x05, 0x8a, 0xf0, 0xff, 0xff,
        0xff, 0xff, 0xcb, 0x7c, 0x40, 0x00, 0xff, 0xff, 0xff, 0xff, 0xd2, 0x3b, 0x3e, 0xf0, 0xff, 0xff, 0xff,
        0xff, 0xd3, 0x8b, 0x7b, 0x80, 0xff, 0xff, 0xff, 0xff, 0xd4, 0x42, 0xad, 0xf0, 0xff, 0xff, 0xff, 0xff,
        0xd5, 0x45, 0x22, 0x00, 0xff, 0xff, 0xff, 0xff, 0xd6, 0x4c, 0xbf, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xd7,
        0x3c, 0xbf, 0x00, 0xff, 0xff, 0xff, 0xff, 0xd8, 0x06, 0x66, 0x70, 0xff, 0xff, 0xff, 0xff, 0xd9, 0x1d,
        0xf2, 0x80, 0xff, 0xff, 0xff, 0xff, 0xd9, 0x41, 0x7c, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xba, 0x52,
        0x20, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x69, 0x9b, 0x90, 0x00, 0x00, 0x00, 0x00, 0x20, 0x7e, 0x84, 0xa0,
        0x00, 0x00, 0x00, 0x00, 0x21, 0x49, 0x7d, 0x90, 0x00, 0x00, 0x00, 0x00, 0x22, 0x67, 0xa1, 0x20, 0x00,
        0x00, 0x00, 0x00, 0x23, 0x29, 0x5f, 0x90, 0x00, 0x00, 0x00, 0x00, 0x24, 0x47, 0x83, 0x20, 0x00, 0x00,
        0x00, 0x00, 0x25, 0x12, 0x7c, 0x10, 0x00, 0x00, 0x00, 0x00, 0x26, 0x27, 0x65, 0x20, 0x00, 0x00, 0x00,
        0x00, 0x26, 0xf2, 0x5e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x28, 0x07, 0x47, 0x20, 0x00, 0x00, 0x00, 0x00,
        0x28, 0xd2, 0x40, 0x10, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02,
        0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x00,
        0x00, 0x71, 0xd7, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x90, 0x01, 0x04, 0x00, 0x00, 0x70, 0x80, 0x00, 0x08,
        0x4c, 0x4d, 0x54, 0x00, 0x43, 0x44, 0x54, 0x00, 0x43, 0x53, 0x54, 0x00, 0x0a, 0x43, 0x53, 0x54, 0x2d,
        0x38, 0x0a}
    toc2, err := time.LoadLocationFromTZData(zone, dataZone)
    if err != nil {
        return err
    }
    /*
       FixedZone使用给定的地点名name和时间偏移量offset(单位秒)创建并返回一个Location。
       使用这个可以自定义时区对象,想怎么玩就怎么玩。
       offset单位为秒,下面表示创建一个偏移8小时的时区,可以传入负数。
    */
    toc3 := time.FixedZone("janbar/test", 8*3600)

    fmt.Println("testZone:")
    fmt.Println(toc0.String(), now.In(toc0))
    fmt.Println(toc1.String(), now.In(toc1))
    fmt.Println(toc2.String(), now.In(toc2))
    fmt.Println(toc3.String(), now.In(toc3))

    fmt.Println("获取时间对象的时区信息:", now.Location().String())
    fmt.Print("获取时间对象的时区信息,以及偏移秒数: ")
    fmt.Println(now.Zone())
    return nil
}

// 从go安装环境里面的zip包获取指定时区文件内容
func getZoneInfoData(zone string) ([]byte, error) {
    z, err := zip.OpenReader(filepath.Join(os.Getenv("GOROOT"), "lib/time/zoneinfo.zip"))
    if err != nil {
        return nil, err
    }
    defer z.Close()

    for _, v := range z.File {
        if v.Name != zone {
            continue
        }
        fr, err := v.Open()
        if err != nil {
            return nil, err
        }
        data, err := ioutil.ReadAll(fr)
        fr.Close()
        if err != nil {
            return nil, err
        }
        return data, nil
    }
    return nil, errors.New(zone + " not find")
}

func byteToString(data []byte) string {
    res := "data := []byte{"
    for i := 0; i < 15; i++ {
        res += fmt.Sprintf("0x%02x,", data[i])
    }
    res += "\n"
    for i, v := range data[15:] {
        res += fmt.Sprintf("0x%02x,", v)
        if (i+1)%17 == 0 {
            res += "\n"
        }
    }
    return res + fmt.Sprintf("\b}")
}

总结

    一次性把golang的time包所有方法都过一遍,妈妈再也不用担心我以后处理时间和日期时出现问题了。其中还包含一些不正确的写法,可能存在隐藏bug啥的。总之语言自带的api还是值得细心研究的,毕竟官方不可能无缘无故提供那些借口吧,总是有各种应用场景需要才会提供。