Go如何处理时间


Go如何处理时间

参考
《Go语言入门经典》23章 Go语言时间编程
理解Golang的Time结构
深入理解GO时间处理(time.Time)
golang 定时任务方面time.Sleep和time.Tick的优劣对比
官方文档time包

开发中经常会使用时间函数,如测试程序的运行时间。Golang提供了标准库 time,提供了一系列时间处理的方法。


0 从一个例子开始

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println(time.Now())
    fmt.Println(time.Now().Date())
}

// 2021-12-16 21:02:30.094398 +0800 CST m=+0.000321334
// 2021 December 16

常见时间单位换算:
1秒=1000毫秒(ms)
1秒=1,000,000 微秒(μs)
1秒=1,000,000,000 纳秒(ns)

上述代码利用 time 打印了当前时间和日期,其中打印日期还用到了类似链式编程的方法,打印出来的时间是什么意思呢?内部是如何实现的呢?下面对此进行分析。



1 基础篇-时间对象

1.1 Time结构体

1.1.1 before go 1.9

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 表示从公元 1年1月1日00:00:00 UTC到要表示的整数秒数,nsec表示余下的纳秒数,loc表示时区。secnsec处理没有歧义的时间值, loc处理偏移量


1.1.2 after go 1.9

因为 2017 年闰一秒, 国际时钟调整, Go 程序两次取time.Now()相减的时间差得到了意料之外的负数, 导致 cloudFlare 的 CDN 服务中断, 详见**https://blog.cloudflare.com/how-and-why-the-leap-second-affected-cloudflare-dns/**, go1.9 在不影响已有应用代码的情况下修改了time.Time的实现。go1.9 的 time.Time 定义为

type Time struct {
    // wall and ext encode the wall time seconds, wall time nanoseconds,
    // and optional monotonic clock reading in nanoseconds.
    //
    // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
    // a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
    // The nanoseconds field is in the range [0, 999999999].
    // If the hasMonotonic bit is 0, then the 33-bit field must be zero
    // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
    // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
    // unsigned wall seconds since Jan 1 year 1885, and ext holds a
    // signed 64-bit monotonic clock reading, nanoseconds since process start.
    wall uint64
    ext  int64

    // 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
}

Time 结构体主要包含三个字段:

  • wall
  • ext
  • loc
Time

Go语言的Time中存储了两种时钟:

  1. Wall Clocks 用来报时,表示墙上挂的钟,即我们平时理解的时间,存储的形式是自 1885 年 1 月 1 日 0 时 0 分 0 秒以来的时间戳(不像很多语言保存的是Unix时间戳即表示到 1970 年 1 月 1 日)。关于这个 1885 年怎么来的,因为 Go 是可以表示超过 int32 的 unixtimestamp 的时间的,1885 应该是扩展出来的能表示的最小值。
  2. Monotonic Clocks 用来测量时间,单调时间,这个时间是自进程启动以来的时间戳,只会增长
tick := time.Tick(1 * time.Second)
for range tick {
    fmt.Println(time.Now())
}

// 2021-12-17 16:57:45.84885 +0800 CST m=+1.007167459
// 2021-12-17 16:57:46.848403 +0800 CST m=+2.006741001
// 2021-12-17 16:57:47.846837 +0800 CST m=+3.005196626
// 2021-12-17 16:57:48.847365 +0800 CST m=+4.005746209
// 2021-12-17 16:57:49.848387 +0800 CST m=+5.006789584
// 2021-12-17 16:57:50.848322 +0800 CST m=+6.006746042

1.2 Location结构体

// A Location maps time instants to the zone in use at that time.
// Typically, the Location represents the collection of time offsets
// in use in a geographical area. For many Locations the time offset varies
// depending on whether daylight savings time is in use at the time instant.
type Location struct {
    name string
    zone []zone
    tx   []zoneTrans

    // The tzdata information can be followed by a string that describes
    // how to handle DST transitions not recorded in zoneTrans.
    // The format is the TZ environment variable without a colon; see
    // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html.
    // Example string, for America/Los_Angeles: PST8PDT,M3.2.0,M11.1.0
    extend string

    // Most lookups will be for the current time.
    // To avoid the binary search through tx, keep a
    // static one-element cache that gives the correct
    // zone for the time when the Location was created.
    // if cacheStart <= t < cacheEnd,
    // lookup can return cacheZone.
    // The units for cacheStart and cacheEnd are seconds
    // since January 1, 1970 UTC, to match the argument
    // to lookup.
    cacheStart int64
    cacheEnd   int64
    cacheZone  *zone
}

要消除时区的影响,可参照世界标准时间(Coordinated Universal Time,UTC)。UTC是时间标准而非时区,它让不同地区的计算机有相同的参照物,而不用考虑相对时区。

如果本地时间比 UTC 时间快,例如中国的时间比 UTC 快 8 小时,就会写作 UTC+8,俗称东八区。相反,如果本地时间比 UTC 时间慢,例如夏威夷的时间比 UTC 时间慢 10 小时,就会写作 UTC-10,俗称西十区。中国虽然横跨 5 个时区(东五~东九),但是按照北京时间进行统一(东八)。




2 入门篇-时间操作

时间操作的方法非常多,建议参考官方文档time

下面主要针对经常使用的时间操作进行介绍:

2.1 time.Now()获取当前时间

fmt.Println(time.Now())
// 2021-12-18 14:21:15.653623 +0800 CST m=+0.000057626

time.Now()返回本地时间,以上结果看出:time.Now()输出默认CST时区时间,Local是东八区的时间所以是+0800CST是中部标准时间在中国是以东八区为标准,m=+0.000057626表示在当前进程的运行时间。

time.Now

2.2 设置时区

loc, _ := time.LoadLocation("America/New_York")
fmt.Println(loc)
fmt.Println(time.Now().In(loc))

// America/New_York
// 2021-12-18 02:25:26.68076 -0500 EST

LoadLocation(name string) (*Location, error)可以设置时区,获得一个时区对象。

In(loc *Location) Time可以传入一个时区对象返回该时区的时间。


2.3 时间运算

时间运算的前提是两个时间属于同一时区。

a, b := time.Now(), time.Now().Add(2*time.Second) // 时间加减
fmt.Println(a)
fmt.Println(b)
fmt.Println(a.After(b)) // a是否在b之后
fmt.Println(a.Before(b)) // a是否在b之后前

// 2021-12-18 15:38:29.399566 +0800 CST m=+0.000076168
// 2021-12-18 15:38:31.399567 +0800 CST m=+2.000076334
// false
// true

2.4 序列化与反序列化

format := "20060102150405"
t1 := time.Now()
fmt.Println(t1) // 2021-12-18 15:57:42.834868 +0800 CST m=+0.000058543
t2 := t1.Format(format) // 20211218155742
fmt.Println(t2)
t3, _ := time.Parse(format, t2) 
fmt.Println(t3) // 2021-12-18 15:57:42 +0000 UTC

// format := "2006-01-02 15:04:05"
// 2021-12-18 15:58:38.113167 +0800 CST m=+0.000058292
// 2021-12-18 15:58:38
// 2021-12-18 15:58:38 +0000 UTC

// format := "2006/01/02 Monday January 03:04:05"
// 2021-12-18 16:03:15.01808 +0800 CST m=+0.000077043
// 2021/12/18 Saturday December 04:03:15
// 2021-12-18 04:03:15 +0000 UTC

// format := "2006年1月2日15点4分5秒 Mon Jan"
// 2021-12-18 16:09:02.987355 +0800 CST m=+0.000073418
// 2021年12月18日16点9分2秒 Sat Dec
// 2021-12-18 16:09:02 +0000 UTC

在按照指定格式打印时间时,首先需要记住一个时间 Mon Jan 2 15:04:05 -0700 MST 20062006年1月2日下午3点4分5秒 星期一,这实际上是凑出来的时间,可能是为了方便记忆,但是为什不直接用yyyy-MM-dd HH:mm:ss呢?

有博客说这是golang开源的时间,这是错的,golang 的生日是 2009 年 11 月 10 日。




3 提升篇-定时器

定时器在开发中用的很多,尤其是并发编程中,context 包中也有用到(超时控制)。

3.1 time.After()

After(d Duration) <-chan Time一般用来控制超时,开一个协程如果运行时间超过 1 秒,select 就会收到 After 通道的数据,将不再接收该协程的结果。

ch := make(chan string)

go func() {
    time.Sleep(time.Second * 2)
    ch <- "result"
}()

select {
case res := <-ch:
    fmt.Println(res)
case <-time.After(time.Second * 1):
    fmt.Println("Timeout!")
}

3.2 time.Tick(d time.Duration)

3.2.1 Tick

使用ticker可让代码每隔特定的时间就重复执行一次。需要在很长的时间内定期执行任务时,这么做很有用,调用Tick函数会返回一个时间类型的channel

ticker := time.Tick(1 * time.Second)
for t := range ticker {
    fmt.Println(t)
}

// 2021-12-18 16:36:00.083872 +0800 CST m=+1.001646043
// 2021-12-18 16:36:01.08731 +0800 CST m=+2.005127543
// 2021-12-18 16:36:02.087241 +0800 CST m=+3.005102168
// 2021-12-18 16:36:03.087233 +0800 CST m=+4.005137335
// ...

time.Tick是对NewTicker的封装,也可以直接使用NewTicker

time.Tick

3.2.2 Sleep

实际上也可以使用time.Sleep(d time.Duration)来实现循环执行的定时任务:

for {
    time.Sleep(1 * time.Second)
    fmt.Println(time.Now())
}

那么两种实现方式之间有什么区别呢?哪种效率更高?


3.2.3 Tick vs Sleep

先说结论

调用Tick函数会返回一个时间类型的channel,如果对channel稍微有些了解的话,我们首先会想到,既然是返回一个channel,在调用 Tick方法的过程中,必然创建了goroutine,该goroutine负责发送数据,唤醒被阻塞的定时任务。

TickSleep,包括time.After函数,都使用的timer结构体,都会被放在同一个协程中统一处理,这样看起来使用TickSleep并没有什么区别。实际上是有区别的,Sleep是使用睡眠完成定时任务,需要被调度唤醒。Tick函数是使用channel阻塞当前协程,完成定时任务的执行。当前并不清楚golang 阻塞和睡眠对资源的消耗会有什么区别,这方面不能给出建议。

但是使用channel阻塞协程完成定时任务比较灵活,可以结合select设置超时时间以及默认执行方法,而且可以设置timer的主动关闭,以及不需要每次都生成一个timer(这方面节省系统内存,垃圾收回也需要时间)。

所以,建议使用time.Tick完成定时任务。

实现原理对比

有空再补,先参考

https://www.cyhone.com/articles/analysis-of-golang-timer/#Timer-%E7%9A%84%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0





文章作者: xuxiangfei
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 xuxiangfei !
  目录