關於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還是值得細心研究的,畢竟官方不可能無緣無故提供那些借口吧,總是有各種應用場景需要才會提供。