關於golang的time包總結

janbar發表於2020-12-16

前言

各種程式語言都少不了與時間有關的操作,因為很多判斷都是基於時間,因此正確和方便的使用時間庫就很重要額。
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還是值得細心研究的,畢竟官方不可能無緣無故提供那些藉口吧,總是有各種應用場景需要才會提供。

相關文章