Golang time包
一、定義
不同於 java 飽受詬病的各種亂七八糟的時間處理相關的包,golang 的 time 包的設計可謂相當精巧。time 包用來描述“時刻”的型別為 Time,其定義如下:
type Time struct { // sec gives the number of seconds elapsed since // January 1, year 1 00:00:00 UTC. sec int64 // nsec specifies a non-negative nanosecond // offset within the second named by Seconds. // It must be in the range [0, 999999999]. nsec int32 // loc specifies the Location that should be used to // determine the minute, hour, month, day, and year // that correspond to this Time. // The nil location means UTC. // All UTC times are represented with loc==nil, never loc==&utcLoc. loc *Location }
如其註釋所述,sec 記錄其距離 UTC 時間0年1月1日0時的秒數, nsec 記錄一個 0~999999999 的納秒數,loc 記錄所在時區。事實上,僅需要 sec 和 nsec 就完全可以描述一個時間點了——“該時間點是距離 UTC 時間0年1月1日0時 sec 秒、nsec 納秒的時間點”——非常準確且非常不容易引起歧義,但這並不符合人們日常生活中描述時間點的方式,我們只會說是某年某月某日,幾點幾分幾秒,然而,一旦要這樣說,事實上就涉及到時區了。
當一個 golang 的 Time 例項被程式設計師問:你記錄的是幾時幾分?這個例項可以說是相當無語的,因為又沒說清是在哪個時區下的幾點幾分,我 TM 怎麼知道是幾點幾分?然而程式設計師並不買賬,因為一個時間點能準確得說出自己是幾點幾分似乎是天經地義的事,於是 Time 類不得不自己記一個時區,預設就是這個無腦的程式設計師所在地的時區(這個值可以向作業系統索要) ,當再次被問幾點幾分的時候,便可以作答了,而記的地方便是 loc 欄位。
2.坑點
站在計算機冰冷的角度來看,“某時區某年某月某日幾點幾分幾秒”是對時間點的人性化描述,而“距離一個眾所周知的時間點多少秒、多少納秒”才是對時間點的準確記錄。這一點,在 Time 型別的實現中展現的淋漓盡致。所以,基於對 Time 型別的瞭解,我們反觀一下對時間的一些操作,看看時區在影響著哪些。
時間的比較、求差操作,很明顯這類操作是與時區無關的,無論 loc 記錄的是什麼,只要對 sec 和 nsec 進行比較、求差,就能得出正確的結果。時間的取時、取分操作,不用說了,肯定是需要時區資訊參與的。
時間的 format 操作,這裡僅指 format 成年月日時分秒的形式,顯然也是需要時區參與的。時間的 parse 操作,即 format 的逆向操作,同樣需要時區參與。
而坑點就在這裡,一方面,format 操作使用 Time 例項記錄的時區,大多數情況下是本地時區;另一方面,parse 操作在並不會預設使用本地時區。
time.Parse() 會嘗試從 value 裡讀出時區資訊,當且僅當:有時區資訊、時區資訊以 zone offset 形式(如+0800)表示、表示結果與本地時區等價時,才會使用本地時區,否則使用讀出的時區。若 value 裡沒有時區資訊,則使用 UTC 時間。這便是第一個坑點。
相比之下,第二個坑點便算不上什麼大事了——不要使用 == 去比較時間是否相等。golang 可沒有什麼過載運算子的說法,使用 == 比較兩個 Time 例項時,事實上就是比較 sec、nsec、loc 三個欄位是否都相等 。然而如我所述,僅需要 sec 和 nsec 就完全可以描述一個時間點了,所以只要這兩個欄位相等,兩個 Time 例項就是指的同一個時間點。而僅因為 loc 值的不同,便判定兩個 Time 例項不相等,這是非常荒謬的。這就是為什麼應該使用 Equal 比較時間點是否相等的原因。
func main() { // format 字串為 年月日時分秒,沒有時區資訊 format := "20060102150405" // t1 沒有寫 time.Now() 是為了避免秒以下單位的時間的影響 // 除此之外和寫 time.Now() 是一樣的 t1 := time.Date(2017, time.November, 30, 0, 0, 0, 0, time.Local) // t1 使用本地時區進行 format,結果是 "20171130000000" // 由進行 parse,由於沒有指定時區,結果是 UTC 時間 2017/11/30 00:00:00 t2, _ := time.Parse(format, t1.Format(format)) // t1 使用本地時區進行 format,結果是 "20171130000000" // t2 使用 UTC 時間進行 format,結果是 "20171130000000" // 所以輸出 true println("1-1 ", t1.Format(format) == t2.Format(format)) // 很顯然不相等,既不是指同一個時間點,時區資訊也不一樣,所以輸出 false println("1-2 ", t1 == t2) // 顯然不相等,t1 和 t2 不是指同一個時間點,所以輸出 false println("1-3 ", t1.Equal(t2)) // t1 使用本地時區進行 format,結果是 "20171130000000" // 由進行 parse,指定了本地時區,結果是本地時間 2017/11/30 00:00:00 t2, _ = time.ParseInLocation(format, t1.Format(format), time.Local) // 顯然相等,輸出 true println("2-1 ", t1.Format(format) == t2.Format(format)) // 既指同一個時間點,時區資訊也一樣,輸出 true println("2-2 ", t1 == t2) // 顯然相等,輸出 true println("2-3 ", t1.Equal(t2)) // 原本 t2 與 t1 完全相等,現在將 t2 改為 UTC 時間 t2 = t2.UTC() // t1 使用本地時區進行 format,結果是 "20171130000000" // t2 使用 UTC 時間進行 format,結果是 "20171129160000" // 所以輸出 false println("3-1 ", t1.Format(format) == t2.Format(format)) // t1 和 t2 表示了相同的時間點,但各自時區資訊不同,所以輸出 false println("3-2 ", t1 == t2) // 由於 t1 和 t2 表示了相同的時間點,所以輸出 true println("3-3 ", t1.Equal(t2)) }
3.在docker中
很明顯,若要避免不必要的麻煩,就要正確地使用 time 包——而這句話的大前提是作業系統的時區設定是正確的,否則一切都是空談。
顯然絕大多數的 PC、伺服器的時區設定肯定是正確(是吧?要不你檢查下?)。需要提高警惕的是 docker 使用者,docker 在編譯映象、啟動容器時均不會繼承宿主機的時區設定。如果容器內的服務對時間不敏感,可能僅是輸出日誌的時間不是本地時間的問題,而如果服務對時間敏感,比如每天早上九點執行某任務,可能就要出錯了。以設為上海時區為例,解決方法有兩個,可視情況取捨。
要麼在映象編譯時指定好時區:
... RUN rm /etc/localtime && ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime ...
要麼在容器啟動時指定好時區:
docker run -e TZ="Asia/Shanghai" -v /etc/localtime:/etc/localtime:ro ...
//獲取當前時間 //2018-07-11 15:07:51.8858085 +0800 CST m=+0.004000001 t := time.Now() fmt.Println(t) //獲取當前時間戳 fmt.Println(t.Unix()) //1531293019 //獲得當前的時間 //2018-7-15 15:23:00 fmt.PrintIn(t.Uninx().Format("2006-01-02 15:04:05")) //時間 to 時間戳 //設定時區 loc, _ := time.LoadLocation("Asia/Shanghai") //2006-01-02 15:04:05是轉換的格式如php的"Y-m-d H:i:s" tt, _ := time.ParseInLocation("2006-01-02 15:04:05", "2018-07-11 15:07:51", loc) fmt.Println(tt.Unix())//1531292871 //時間戳 to 時間 tm := time.Unix(1531293019, 0) //2018-07-11 15:10:19 fmt.Println(tm.Format("2006-01-02 15:04:05")) //獲取當前年月日,時分秒 y := t.Year()//年 m := t.Month()//月 d := t.Day()//日 h := t.Hour()//小時 i := t.Minute()//分鐘 s := t.Second()//秒 //2018 July 11 15 24 59 fmt.Println(y, m, d, h, i, s) }
更多參考Golang的時間生成,格式化,以及獲取函式執行時間的方法
// After waits for the duration to elapse and then sends the current time // on the returned channel. // It is equivalent to NewTimer(d).C. // The underlying Timer is not recovered by the garbage collector // until the timer fires. If efficiency is a concern, use NewTimer // instead and call Timer.Stop if the timer is no longer needed. func After(d Duration) <-chan Time { return NewTimer(d).C }
直譯就是:
等待引數duration時間後,向返回的chan裡面寫入當前時間。和NewTimer(d).C效果一樣,直到計時器觸發,垃圾回收器才會恢復基礎計時器。如果擔心效率問題, 請改用 NewTimer, 然後呼叫計時器. 不用了就停止計時器。
解釋一下,是什麼意思呢?
就是呼叫time.After(duration),此函式馬上返回,返回一個time.Time型別的Chan,不阻塞。後面你該做什麼做什麼,不影響。到了duration時間後,自動塞一個當前時間進去。你可以阻塞的等待,或者晚點再取。因為底層是用NewTimer實現的,所以如果考慮到效率低,可以直接自己呼叫NewTimer。
package main import ( "time" "fmt" ) func main(){ tchan := time.After(time.Second*3) fmt.Printf("tchan type=%T\n",tchan) fmt.Println("mark 1") fmt.Println("tchan=",<-tchan) fmt.Println("mark 2") }
上面的例子執行結果如下
tchan type=<-chan time.Time mark 1 tchan= 2018-03-15 09:38:51.023106 +0800 CST m=+3.015805601 mark 2
首先瞬間打印出前兩行,然後等待3S,列印後後兩行。