Go 语言
标准库、反射和 cgo
time 的用法

time 的用法

我们编写的大部分现代应用程序离不开与时间相关的操作。 常见的时间操作有获取当前时间、时间比较、时区相关的时间操作、时间格式化、定时器(一次性定时器和重复定时器)等等。 Go 语言通过标准库的 time 包为常见时间操作提供了全面的支持。

时间的基础操作

获取当前时间

func main() {
    t := time.Now()
    fmt.Println(t) // 2024-02-14 15:04:05.999999999 +0800 CST
}

我们看到,time.Now() 返回了一个 time.Time 类型的值,time 包将 Time 类型用作一个即使时间(time instant)的抽象。结构体如下:

type Time struct {
    wall uint64
    ext  int64
    loc  *Location
}

其中 wallext 两个字段分别表示时间的秒数和纳秒数,loc 字段表示时区。由三个字段组成的 Time 结构体要同时表示两种时间:挂钟时间(wall time)和单调时间(monotonic time)。

挂钟时间主要用于告知当前时间。

单调时间表示的是程序进程启动之后流逝的时间,两次采集的单调时间的之差不可能为负数。

获取时区的当前时间

获取特定时区的当前时间有下面几种方法:

1、设置 TZ 环境变量

func main() {
    os.Setenv("TZ", "Asia/Shanghai")
    t := time.Now()
    fmt.Println(t) // 2024-02-14 15:04:05.999999999 +0800 CST
}

2、使用 time.LoadLocation 函数

func main() {
    loc, _ := time.LoadLocation("Asia/Shanghai")
    t := time.Now().In(loc)
    fmt.Println(t) // 2024-02-14 15:04:05.999999999 +0800 CST
}

3、使用 time.FixedZone 函数

func main() {
    loc := time.FixedZone("CST", 8*3600)
    t := time.Now().In(loc)
    fmt.Println(t) // 2024-02-14 15:04:05.999999999 +0800 CST
}

时间的比较与运算

Time 类型不能直接用 ==!= 进行比较,同时也不能用作 map 类型的 key 值。但是 time 包提供了 Equal、Before、After 三个方法用于时间的比较。

我们来看一下 Equal、Before、After 的例子:

func main() {
    t1 := time.Now()
    t2 := time.Now()
    fmt.Println(t1.Equal(t2))  // false
    fmt.Println(t1.Before(t2))  // true
    fmt.Println(t1.After(t2))   // false
}

time 包还提供了 Add、Sub 两个方法用于时间的加减运算。

func main() {
    t := time.Now()
    t1 := t.Add(24 * time.Hour)
    fmt.Println(t1) // 2024-02-15 15:04:05.999999999 +0800 CST
    t2 := t.Add(-24 * time.Hour)
    fmt.Println(t2) // 2024-02-13 15:04:05.999999999 +0800 CST
 
    d := t1.Sub(t)
    fmt.Println(d) // 24h0m0s
 
    time.Sleep(3 * time.Second)
    t3 := time.Now()
    dd := t3.Sub(t)
    fmt.Println(dd) // 3.000000003s
}

Sub 方法对两个 Time 实例的差值处理也分为两种情况:如果两个实例都包含有单调时间,那么返回的是 ext 字段的差值,否则分别算出整秒部分的差与非整秒部分的差,然后返回两者之和。

时间的格式化

Go 语言采用了更为直观的参考时间(reference time)替代 strftime 的各种标准占位符,使用参考时间构造出来的时间格式串与最终输出串是一模一样的。

func main() {
    t := time.Now()
    fmt.Println(t.Format("2006年01月02日 15时04分05秒")) // 2024年02月14日 15时04分05秒
}

Go 文档给出的标准的参考时间如下:

const (
    ANSIC       = "Mon Jan _2 15:04:05 2006"
    UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
    RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
    RFC822      = "02 Jan 06 15:04 MST"
    RFC822Z     = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
    RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
    RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
    RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
    RFC3339     = "2006-01-02T15:04:05Z07:00"
    RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
    Kitchen     = "3:04PM"
    // Handy time stamps.
    Stamp      = "Jan _2 15:04:05"
    StampMilli = "Jan _2 15:04:05.000"
    StampMicro = "Jan _2 15:04:05.000000"
    StampNano  = "Jan _2 15:04:05.000000000"
)

定时器

time 包提供两类定时器:一次性定时器 Timer 和重复定时器 Ticker。

time 包提供多种场景 Timer 定时器的方式,如下:

func createTimerByAfterFunc() {
    time.AfterFunc(2*time.Second, func() {
        fmt.Println("after 2 seconds")
    })
}
 
func createTimerByNewTimer() {
    t := time.NewTimer(2 * time.Second)
    select {
    case <-t.C:
        fmt.Println("after 2 seconds")
    }
}
 
func createTimerByAfter() {
    select {
    case <-time.After(2 * time.Second):
        fmt.Println("after 2 seconds")
    }
}
 
func main() {
    createTimerByAfterFunc()
    createTimerByNewTimer()
    createTimerByAfter()
}

无论采用了哪种方式创建 Timer,本质上都是在用户层实例化一个 Timer 结构体:

type Timer struct {
    C <-chan Time
    r runtimeTimer
}
 
func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1)
    t := &Timer{
        C: c,
        r: runtimeTimer{
            when: when(d),
            f:    sendTime,
            arg:  c,
        },
    }
    startTimer(&t.r)
    return t
}

Timer 提供了 Stop 方法来将尚未触发的定时器从 P 中的最小堆中移除,使之失效,可以减小最小堆的管理和垃圾回收的压力。

func consume(c <- chan bool) bool {
    t := time.NewTimer(2 * time.Second)
    defer t.Stop()
    select {
    case b := <-c:
        if b == false {
            return true
        }
        return false
    case <-t.C:
        fmt.Println("after 2 seconds")
        return true
    }
}

定时器也是可以重用的,需要用到 Reset 方法。Go 官方文档值对如下两种定时器调用 Reset 方法:

  • 已经停止了的定时器
  • 已经触发过且 Time.C 中的数据已经被读空了。

推荐使用的模式:

if !t.Stop() {
    <-t.C
}
t.Reset(2 * time.Second)

我们改造一下上面的例子:

func consume(c <- chan bool, t *time.Timer) bool {
    if !t.Stop() {
        select {
        case <-t.C:
        default:
        }
    }
    t.Reset(2 * time.Second)
 
    select {
    case b := <-c:
        if b == false {
            return true
        }
        return false
    case <-t.C:
        fmt.Println("after 2 seconds")
        return true
    }
}

上面代码跟官方推荐的模式区别在于多了 select 和 default 处理,当 time.C 中无数据时,代码可以通过 default 分支继续往下处理,而不会再阻塞在对 time.C 的读取上。