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