1. 程式人生 > 其它 >Go 語言標準庫之 time 包

Go 語言標準庫之 time 包

Go 語言的 time 包提供了時間的顯示和測量用的函式。日曆的計算採用的是公曆。

Location 型別

在 Go 語言中,表示時區的型別是type Location struct{...},程式碼中使用*time.Location物件。

// Location 代表一個(關聯到某個時間點的)地點,以及該地點所在的時區
type Location struct {
    // 內含隱藏或非匯出欄位
}

// Local 代表系統本地,對應本地時區
var Local *Location = &localLoc

// UTC 代表通用協調時間,對應零時區
var UTC *Location = &utcLoc

// 返回使用給定的名字建立的 Location
// 如果 name 是 "" 或 "UTC",返回 UTC;如果 name 是 "Local",返回 Local;
// 否則 name 應該是 IANA 時區資料庫裡有記錄的地點名,如 "America/New_York"
func LoadLocation(name string) (*Location, error)

// 自定義地點和時區名稱 name,在零時區基礎上根據時間偏移量 offset(單位秒)建立並返回一個 Location
func FixedZone(name string, offset int) *Location

// 返回對時區資訊的描述,返回值繫結為 LoadLocation 或 FixedZone 函式建立 l 時的 name 引數
func (l *Location) String() string

示例程式碼如下:

package main

import (
    "fmt"
    "time"
)

func main() {
    // 獲取本地當前時間
    now := time.Now()

    // 獲取本地時區
    // 方式一
    loc1 := time.Local
    fmt.Println(loc1.String(), now.In(loc1)) // Local 2021-12-04 14:19:42.8397569 +0800 CST
    // 方式二
    loc2, _ := time.LoadLocation("Local")
    fmt.Println(loc2.String(), now.In(loc2)) // Local 2021-12-04 14:19:42.8397569 +0800 CST

    // 獲取零時區
    // 方式一
    utc1 := time.UTC
    fmt.Println(utc1.String(), now.In(utc1)) // UTC 2021-12-04 06:19:42.8397569 +0000 UTC
    // 方式二
    utc2, _ := time.LoadLocation("")
    fmt.Println(utc2.String(), now.In(utc2)) // UTC 2021-12-04 06:19:42.8397569 +0000 UTC
    // 方式三
    utc3, _ := time.LoadLocation("UTC")
    fmt.Println(utc3.String(), now.In(utc3)) // UTC 2021-12-04 06:19:42.8397569 +0000 UTC

    // 指定地點和時區,東八區
    shanghai, _ := time.LoadLocation("Asia/Shanghai")
    fmt.Println(shanghai.String(), now.In(shanghai)) // Asia/Shanghai 2021-12-04 14:19:42.8397569 +0800 CST

    // 自定義地點和時區名稱,東八區
    beijing := time.FixedZone("Beijing", 8*60*60)
    fmt.Println(beijing.String(), now.In(beijing)) // Beijing 2021-12-04 14:19:42.8397569 +0800 Beijing
}

Time 型別

Time型別表示時間,我們可以通過以下函式獲取時間物件,然後獲取時間物件的年月日時分秒等資訊。

獲取 Time 物件

// Time 代表一個納秒精度的時間點
type Time struct {
    // 內含隱藏或非匯出欄位
}

// 返回本地當前時間
func Now() Time

// 返回一個時區為 loc、當地時間為 year-month-day hour:min:sec + nsec nanoseconds 的時間
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time

// 建立一個本地時間,對應 sec 和 nsec 表示的 Unix 時間(從 January 1, 1970 UTC 至該時間的秒數和納秒數)
func Unix(sec int64, nsec int64) Time

// 返回採用本地地點和本地時區,但指向同一時間點的 Time
func (t Time) Local() Time

// 返回採用 UTC 和零時區,但指向同一時間點的 Time
func (t Time) UTC() Time

// 返回採用 loc 指定的地點和時區,但指向同一時間點的 Time
func (t Time) In(loc *Location) Time

示例程式碼如下:

package main

import (
    "fmt"
    "time"
)

func main() {
    // Now:獲取本地當前時間
    now := time.Now()
    fmt.Println(now) // 2021-12-04 14:43:13.5010095 +0800 CST m=+0.004735101

    // Date:自定義時間
    setTime1 := time.Date(2021, 12, 4, 12, 3, 56, 0, now.Location())
    fmt.Println(setTime1) // 2021-12-04 12:03:56 +0800 CST

    // Unix:自定義時間
    setTime2 := time.Unix(setTime1.Unix(), 1000).In(now.Location())
    fmt.Println(setTime2) // 2021-12-04 12:03:56.000001 +0800 CST
}

獲取 Time 基本資訊

// Weekday 代表一週的某一天
type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

// String 返回該日(周幾)的英文名("Sunday"、"Monday",……)
func (d Weekday) String() string
// Month代表一年的某個月
type Month int

const (
    January Month = 1 + iota
    February
    March
    April
    May
    June
    July
    August
    September
    October
    November
    December
)

// String 返回月份的英文名("January","February",……)
func (m Month) String() string 
// 返回 t 的地點和時區資訊
func (t Time) Location() *Location

// 計算 t 所在的時區,返回該時區的規範名(如"CET")和該時區相對於 UTC 的時間偏移量(單位秒)
func (t Time) Zone() (name string, offset int)

// 將 t 表示為 Unix 時間,即從時間點 January 1, 1970 UTC 到時間點 t 所經過的時間(單位秒)
func (t Time) Unix() int64

// 將 t 表示為 Unix 時間,即從時間點 January 1, 1970 UTC 到時間點 t 所經過的時間(單位納秒)
func (t Time) UnixNano() int64

// 返回時間點 t 對應的年、月、日
func (t Time) Date() (year int, month Month, day int)

// 返回 t 對應的那一天的時、分、秒
func (t Time) Clock() (hour, min, sec int)

// 返回時間點 t 對應的年份
func (t Time) Year() int

// 返回時間點 t 對應那一年的第幾月
func (t Time) Month() Month

// 返回時間點 t 對應的 ISO 9601 標準下的年份和星期編號,星期編號範圍 [1,53]
// 1 月 1 號到 1 月 3 號可能屬於上一年的最後一週,12 月 29 號到 12 月 31 號可能屬於下一年的第一週
func (t Time) ISOWeek() (year, week int)

// 返回時間點 t 對應的那一年的第幾天,平年的返回值範圍 [1,365],閏年 [1,366]
func (t Time) YearDay() int

// 返回時間點 t 對應那一月的第幾日
func (t Time) Day() int

// 返回時間點 t 對應的那一週的周幾
func (t Time) Weekday() Weekday

// 返回 t 對應的那一天的第幾小時,範圍 [0, 23]
func (t Time) Hour() int

// 返回 t 對應的那一小時的第幾分種,範圍 [0, 59]
func (t Time) Minute() int

// 返回 t 對應的那一分鐘的第幾秒,範圍 [0, 59]
func (t Time) Second() int

// 返回 t 對應的那一秒內的納秒偏移量,範圍 [0, 999999999]
func (t Time) Nanosecond() int

示例程式碼如下:

package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()

    // 地點和時區資訊
    fmt.Println(now.Location()) // Local
    // 時區
    fmt.Println(now.Zone()) // CST 28800
    // Unix 時間(單位秒)
    fmt.Println(now.Unix()) // 1638602223
    // Unix 時間(單位納秒)
    fmt.Println(now.UnixNano())   // 1638602223393227200
    fmt.Println(now.Date())       // 2021 December 4
    fmt.Println(now.Clock())      // 15 17 3
    fmt.Println(now.Year())       // 2021
    fmt.Println(now.Month())      // December
    fmt.Println(now.ISOWeek())    // 2021 48
    fmt.Println(now.YearDay())    // 338
    fmt.Println(now.Day())        // 4
    fmt.Println(now.Weekday())    // Saturday
    fmt.Println(now.Hour())       // 15
    fmt.Println(now.Minute())     // 17
    fmt.Println(now.Second())     // 3
    fmt.Println(now.Nanosecond()) // 393227200
}

Time 比較和運算

// Duration 型別代表兩個時間點之間經過的時間,以納秒為單位。可表示的最長時間段大約 290 年
type Duration int64 

const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)

// ParseDuration 解析一個時間段字串
// 一個時間段字串是一個序列,每個片段包含可選的正負號、十進位制數、可選的小數部分和單位字尾,如"300ms"、"-1.5h"、"2h45m"。
// 合法的單位有"ns"、"us" /"µs"、"ms"、"s"、"m"、"h"
func ParseDuration(s string) (Duration, error)

// Hours 將時間段表示為 float64 型別的小時數
func (d Duration) Hours() float64

// Hours 將時間段表示為 float64 型別的分鐘數
func (d Duration) Minutes() float64

// Hours 將時間段表示為 float64 型別的秒數
func (d Duration) Seconds() float64

// Hours 將時間段表示為 int64 型別的納秒數,等價於 int64(d)
func (d Duration) Nanoseconds() int64

// 返回時間段採用 "72h3m0.5s" 格式的字串表示
func (d Duration) String() string

// 表示按照 m 給定的單位,返回四捨五入
func (d Duration) Round(m Duration) Duration

// 表示按照 m 給定的單位,返回舍尾數計算
func (d Duration) Truncate(m Duration) Duration
// 報告 t 是否代表 Time 零值的時間點,January 1, year 1, 00:00:00 UTC
func (t Time) IsZero() bool

// 判斷兩個時間是否相同,會考慮時區的影響,因此不同時區標準的時間也可以正確比較
// 本方法和用 t == u 不同,這種方法還會比較地點和時區資訊
func (t Time) Equal(u Time) bool

// 如果 t 代表的時間點在 u 之前,返回真;否則返回假
func (t Time) Before(u Time) bool

// 如果 t 代表的時間點在 u 之後,返回真;否則返回假
func (t Time) After(u Time) bool

// 返回時間點 t + d
func (t Time) Add(d Duration) Time

// 返回增加了給出的年份、月份和天數的時間點 Time。例如,時間點 January 1, 2011 呼叫 AddDate(-1, 2, 3) 會返回 March 4, 2010
// AddDate 會將結果規範化,類似 Date 函式的做法。因此,舉個例子,給時間點 October 31 新增一個月,會生成時間點 December 1
func (t Time) AddDate(years int, months int, days int) Time

// 返回一個時間段 t - u。如果結果超出了 Duration 可以表示的最大值/最小值,將返回最大值/最小值
// 要獲取時間點 t-d(d 為 Duration),可以使用 t.Add(-d)
func (t Time) Sub(u Time) Duration

// 返回從 t 到現在經過的時間,等價於 time.Now().Sub(t)
func Since(t Time) Duration

// 返回從現在到 t 經過的時間,等價於 t.Sub(time.Now())
func Until(t Time) Duration

示例程式碼如下:

package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()
    time1 := time.Date(2021, 12, 4, 12, 3, 56, 0, now.Location())

    fmt.Println(now.IsZero())            // false
    fmt.Println(now.Equal(time1))        // false
    fmt.Println(now.Before(time1))       // false
    fmt.Println(now.After(time1))        // true
    fmt.Println(now.Add(time.Hour))      // 2021-12-04 17:16:41.2619698 +0800 CST m=+3600.006734601
    fmt.Println(now.Add(-time.Hour))     // 2021-12-04 15:16:41.2619698 +0800 CST m=-3599.993265399
    fmt.Println(now.AddDate(1, 1, 1))    // 2023-01-05 16:16:41.2619698 +0800 CST
    fmt.Println(now.AddDate(-1, -1, -1)) // 2020-11-03 16:16:41.2619698 +0800 CST
    fmt.Println(now.Sub(time1))          // 4h12m45.2619698s
    fmt.Println(time.Since(time1))       // 4h12m45.2843379s
    fmt.Println(time.Until(time1))       // -4h12m45.2843379s
}

Time 與 string 轉換

// 解析並返回一個格式化的時間字串所代表的時間,缺少時區資訊時,Parse 會將時區設定為 UTC 時間
// layout 定義了參考時間:Mon Jan 2 15:04:05 -0700 MST 2006
func Parse(layout, value string) (Time, error)

// ParseInLocation 類似 Parse 但有兩個重要的不同之處
// 第一,當缺少時區資訊時,Parse 將時區設定為 UTC 時間,而 ParseInLocation 將返回值的 Location 設定為loc;
// 第二,當時間字串提供了時區偏移量資訊時,Parse 會嘗試去匹配本地時區,而 ParseInLocation 會去匹配loc
func ParseInLocation(layout, value string, loc *Location) (Time, error)

// 將時間字串轉換為 time.Duration 物件
// 字串可以使用這些單位: "ns", "us" (or "µs"), "ms", "s", "m", "h"
func ParseDuration(s string) (Duration, error)

// 根據 layout 指定的格式返回 t 代表的時間點的格式化文字表示
// layout 定義了參考時間:Mon Jan 2 15:04:05 -0700 MST 2006
func (t Time) Format(layout string) string

// 該方法為基本格式化時間物件為字串,用該方法可以避免申請臨時物件
// 需要由使用者控制傳入 buf 的長度,如果長度不夠,則返回值是擴容後的可用的資料,而 buf 裡面資料無效
func (t Time) AppendFormat(b []byte, layout string) []byte

// 返回採用如下格式字串的格式化時間:"2006-01-02 15:04:05.999999999 -0700 MST"
func (t Time) String() string

示例程式碼如下:

package main

import (
    "fmt"
    "time"
)

func main() {
    // Go 語言格式化時間模板不是常見的 Y-m-d H:M:S,而是一個固定時間,記憶口訣為 2006 1 2 3 4
    const timeFormat = "2006-01-02 15:04:05"
    now := time.Now()

    // 將字串轉換換為時間
    fmt.Println(time.Parse(timeFormat, "2021-12-04 10:53:58"))                           // 2021-12-04 10:53:58 +0000 UTC <nil>
    fmt.Println(time.ParseInLocation(timeFormat, "2021-12-04 10:53:58", now.Location())) // 2021-12-04 10:53:58 +0800 CST <nil>
    fmt.Println(time.ParseDuration("24h5m6s123ms456us321ns"))                            // 24h5m6.123456321s <nil>

    // 將當前時間轉換為字串
    fmt.Println(now.Format(timeFormat)) // 2021-12-04 16:56:38
    buf := make([]byte, 0, 64)
    fmt.Printf("%s\n", now.AppendFormat(buf, timeFormat)) // 2021-12-04 16:56:38
}

Timer 型別

// Timer 型別代表單次時間事件
// 當 Timer 到期時,當時的時間會被髮送給 C,除非 Timer 是被 AfterFunc 函式建立的。
type Timer struct {
    C <-chan Time
    // 內含隱藏或非匯出欄位
}

// 建立一個 Timer,它會在最少過去時間段 d 後到期,向其自身的 C 欄位傳送當時的時間
func NewTimer(d Duration) *Timer

// 另起一個 Go 協程等待時間段 d 過去,然後呼叫 f
// 它返回一個 Timer,可以通過呼叫其 Stop 方法來取消等待和對 f 的呼叫。
func AfterFunc(d Duration, f func()) *Timer

// 使 t 重新開始計時,(本方法返回後再)等待時間段 d 過去後到期
// 如果呼叫時 t 還在等待中會返回真;如果 t 已經到期或者被停止了會返回假
func (t *Timer) Reset(d Duration) bool

// 停止 Timer 的執行。如果停止了 t 會返回真;如果 t 已經被停止或者過期了會返回假
// Stop 不會關閉通道 t.C,以避免從該通道的讀取不正確的成功
func (t *Timer) Stop() bool

// Sleep 阻塞當前 Go 協程至少 d 代表的時間段。d <= 0 時,Sleep 會立刻返回
func Sleep(d Duration)

示例程式碼如下:

package main

import (
    "fmt"
    "time"
)

func main() {
    // 大多數場景用下面這種方式處理超時邏輯
    // 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:
    }
}

Ticker 型別

// Ticker 保管一個通道,並每隔一段時間向其傳遞 "tick"
type Ticker struct {
    C <-chan Time // 週期性傳遞時間資訊的通道
    // 內含隱藏或非匯出欄位
}

// 返回一個新的 Ticker,該 Ticker 包含一個通道欄位,並會每隔時間段 d 就向該通道傳送當時的時間
// 它會調整時間間隔或者丟棄 tick 資訊以適應反應慢的接收者。如果 d<=0 會 panic。關閉該 Ticker 可以釋放相關資源
func NewTicker(d Duration) *Ticker

// 關閉一個 Ticker。在關閉後,將不會發送更多的 tick 資訊
// Stop 不會關閉通道 t.C,以避免從該通道的讀取不正確的成功
func (t *Ticker) Stop()

// 會在另一執行緒經過時間段 d 後向返回值傳送當時的時間。等價於 NewTimer(d).C
func After(d Duration) <-chan Time

// Tick 是 NewTicker 的封裝,只提供對 Ticker 的通道的訪問。如果不需要關閉 Ticker,本函式就很方便。
func Tick(d Duration) <-chan Time

示例程式碼如下:

package main

import (
    "fmt"
    "time"
)

func main() {
    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()
}

參考

  1. 關於golang的time包總結
  2. Go 語言 time 包常用用法筆記
  3. golang time包的用法詳解