Golang time包

一、golang time 包的坑
1.定义

不同于 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 ...
二、示例
1.golang时间戳和时间的转化

Golang 输出格式化的时间,以及时间相关的一些方法

   //获取当前时间
   //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) 
}

//长度为10的时间戳是以“秒”为单位; 
//长度为13位数的时间戳是以“毫秒”为单位; 
//长度为19位数的时间戳是以“纳秒”为单位;

fmt.Printf("Now is %v\n", time.Now().Unix())    //秒
fmt.Printf("Now is %v\n", time.Now().UnixNano())   //纳秒
fmt.Printf("Now is %v\n", time.Now().UnixNano()/1e6)  //纳秒转毫秒
fmt.Printf("Now is %v\n", time.Now().UnixNano()/1e9)   //纳秒转秒
//输出:

//Now is 1536631685
//Now is 1536631685040620400
//Now is 1536631685040
//Now is 1536631685

更多参考 Golang的时间生成,格式化,以及获取函数执行时间的方法

2.Golang时区设置

在Go语言上,go语言的time.Now()返回的是当地时区时间,直接用:time.Now().Format("2006-01-02 15:04:05")输出的是当地时区时间。

go语言并没有全局设置时区这么一个东西,每次输出时间都需要调用一个In()函数改变时区:

var cstSh, _ = time.LoadLocation("Asia/Shanghai") //上海
fmt.Println("SH : ", time.Now().In(cstSh).Format("2006-01-02 15:04:05"))

在windows系统上,没有安装go语言环境的情况下,time.LoadLocation会加载失败。

var cstZone = time.FixedZone("CST", 8*3600)       // 东八
fmt.Println("SH : ", time.Now().In(cstZone).Format("2006-01-02 15:04:05"))

最好的办法是用time.FixedZone

三、定时器

1.GO-time.after 用法

// 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,打印后后两行。

2.Golang 定时器timer和ticker

func timer1() {
    fmt.Println("loop begin",time.Now())
    //timer1 := time.NewTimer(3 * time.Second)
    timer1 := time.NewTicker(3 * time.Second)
    for {
        select {
        case <-timer1.C:
            fmt.Println("loop loop",time.Now())
            //timer1.Reset(2*time.Second)
        }
    }
}
func main() {

        d := time.Duration(time.Second*2)

        t := time.NewTimer(d)
        defer t.Stop()

        for {
                <- t.C

                fmt.Println("timeout...")
        // need reset
        t.Reset(time.Second*2)
        }
}

使用timer定时器,超时后需要重置,才能继续触发。参考golang的一些坑---timer篇[Golang] timer可能造成的内存泄漏

go func(){ <-timer.C //读取timer的channel,当timer到期时channel会读取到数据,向下执行逻辑,但当timer.stop()被调用时,这个channel并不会读取到数据,导致一直hung在这里,goroutine不会退出 do something....}

4.golang中使用timer的三种方式

// (A)
time.AfterFunc(5 * time.Minute, func() {
    fmt.Printf("expired")
}

// (B) create a Timer object
timer := time.NewTimer(5 * time.Minute)
<-timer.C
fmt.Printf("expired")

// (C) time.After() returns timer.C internally
<-time.After(5 * time.Minute)
fmt.Printf("expired")

注意AfterFunc是在另外一个协程里,参考Go语言中的定时器

3.go里面select-case和time.Ticker的使用注意事项
问题出在这个select里面:

select {
case ch <- i:
case <-tick.C:
fmt.Printf("%d: case <-tick.C\n", i)
}

当两个case条件都满足的时候,运行时系统会通过一个伪随机的算法决定哪个case将会被执行
所以当tick.C条件满足的那个循环,有某种概率造成ch<-i没有发送(虽然通道两端没有阻塞,满足发送条件)

4.从99.9%CPU浅谈Golang的定时器实现原理

//场景1:
for {
    select {
    case <- time.After(10 * time.Microsecond):
        fmt.Println("hello timer")
    }
}

//场景2:
for {
    select {
    case <- time.Tick(10 * time.Microsecond):
        fmt.Println("hello, tick")
    }
}

从第3节的源码中我们可以看到After和Tick其实是一个创建了一个单次的timer一个是创建了一个永久性的timer。因此场景2中Tick的用法会导致进程中创建无数个Tick,这最终导致了timer处理线程忙死。因此,使用Tick进行定时任务的话我们可以将Tick对象建在循环外面:

    tick := time.Tick(10 * time.Microsecond)
    for {
        select {
        case <- tick:
            fmt.Printf("hello, tick 2")
        }
    }

其次golang的处理方式中也可以看出,go的timer的处理和用户端程序定义的间隔时间不一定完全精准,用户的回调函数执行时间越长单个timer对堆中其他邻近timer的影响越大。因此timer的回调函数一定是执行时间越短越好。

5.golang中timer定时器实现原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容