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
}
其中 wall
和 ext
两个字段分别表示时间的秒数和纳秒数,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
的读取上。