CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
  • Go开发实用指南 说明
  • 第1章 Go项目如何组织
  • 第2章 字符串处理
  • 第3章 处理日期和时间
    • 处理Unix时间
      • 操作方法……
    • 日期/时间的组成部分
      • 操作方法……
    • 日期/时间运算
      • 操作方法……
      • 工作原理……
    • 日期/时间的格式化与解析
      • 操作方法……
    • 处理时区
      • 操作方法……
      • 它的工作原理……
      • 存储时间信息
      • 如何操作……
    • 定时器(Timers)
      • 如何操作……
      • 它的工作原理……
    • 时间 ticker(Tickers)
      • 如何操作……
      • 它的工作原理……
  • 第4章 使用数组、切片和映射
  • 第5章 使用类型、结构体和接口
  • 第6章 使用泛型
  • 第7章 并发
  • 第8章 错误与恐慌(panic)
  • 第9章 context包
  • 第10章 处理大量数据
  • 第11章 处理JSON数据
  • 第12章 进程
  • 第13章 网络编程
  • 第14章 流式输入/输出
  • 第15章 数据库
  • 第16章 日志记录
  • 第17章 测试、基准测试和性能分析
目录

第3章 处理日期和时间

# 第3章 处理日期和时间

在任何编程语言中,处理日期和时间都可能颇具难度。Go语言的标准库提供了易于使用的工具来处理日期和时间相关的操作。这些工具可能与许多人习惯的有所不同。例如,不同语言的一些库会区分时间类型(time type)和日期类型(date type),而Go语言的标准库仅包含time.Time这一种类型。这可能会让你在使用Go语言处理时间时感到有些困惑。

我认为Go语言对日期/时间的处理方式降低了产生潜在bug的可能性。你看,当你谈论时间时,必须非常谨慎且明确你的意思:你说的是某个时间点还是一个时间段?日期实际上是一个时间段(例如,2024年8月1日从2024年8月1日00:00:00开始,一直持续到2024年8月1日23:59:59),尽管通常人们并非这样想。一个具体的日期/时间值还取决于你测量时间的地点。美国科罗拉多州丹佛市的2023年11月5日08:00,与德国柏林的2023年11月5日08:00是不同的。时间总是向前推进,但日期/时间可能会跳过或倒退:在美国科罗拉多州丹佛市,2023年11月5日02:59之后,时间会倒退回2023年11月5日02:00,因为这是科罗拉多州夏令时结束的时间。所以,实际上2023年11月5日02:10:10有两个时间实例,一个是山区夏令时,另一个是山区标准时间。

如今,在生产环境中有许多软件bug是由于时间处理不当导致的。例如,如果你要计算客户订阅的到期时间,就必须考虑客户所在的位置以及订阅到期的具体时间,否则,在订阅的最后一天,他们的订阅可能会提前(或延迟)到期。

本章包含以下正确处理日期/时间的方法:

  • 处理Unix时间
  • 日期/时间的组成部分
  • 日期/时间运算
  • 日期/时间的格式化与解析
  • 处理时区
  • 定时器(Timers)
  • 间隔定时器(Tickers)
  • 存储时间信息

# 处理Unix时间

Unix时间是指从1970年1月1日协调世界时(UTC,即纪元时间)开始经过的秒数(或毫秒数、微秒数、纳秒数 )。Go语言使用int64类型来表示这些值,所以以秒为单位的Unix时间可以表示过去或未来数十亿年的时间。以纳秒为单位的Unix时间可以表示1678年到2262年之间的日期值。Unix时间是对某个时间实例的绝对度量,即从纪元时间开始(或到纪元时间)的持续时间。它与地点无关,所以对于两个Unix时间s和t,如果s < t,那么无论在什么地点,s都发生在t之前。由于这些特性,Unix时间通常用作时间戳(timestamp),用于标记事件发生的时间(例如写入日志、插入记录等)。

# 操作方法……

  • 要获取当前的Unix时间,可使用以下方法:
    • time.Now().Unix() int64:以秒为单位的Unix时间
    • time.Now().UnixMilli() int64:以毫秒为单位的Unix时间
    • time.Now().UnixMicro() int64:以微秒为单位的Unix时间
    • time.Now().UnixNano() int64:以纳秒为单位的Unix时间
  • 给定一个Unix时间,可使用以下方法将其转换为time.Time类型:
    • time.Unix(sec, nanosec int64) time.Time:将以秒和/或纳秒为单位的Unix时间转换为time.Time类型
    • time.UnixMilli(int64) time.Time:将以毫秒为单位的Unix时间转换为time.Time类型
    • time.UnixMicro(int64) time.Time:将以微秒为单位的Unix时间转换为time.Time类型
  • 要将Unix时间转换为本地时间,可使用localTime := time.Unix(unixTimeSeconds,0).In(location),其中location是一个*time.Location类型,用于指定解释Unix时间的地点。

# 日期/时间的组成部分

在处理日期值时,你经常需要根据其组成部分构建一个日期/时间,或者需要访问一个日期/时间值的各个组成部分。本方法展示了如何实现这些操作。

# 操作方法……

  • 要根据各个部分构建一个日期/时间值,可使用time.Date函数。
  • 要获取一个日期/时间值的各个部分,可使用time.Time的方法:
    • time.Day() int
    • time.Month() time.Month
    • time.Year() int
    • time.Date() (year, month, day int)
    • time.Hour() int
    • time.Minute() int
    • time.Second() int
    • time.Nanosecond() int
    • time.Zone() (name string,offset int)
    • time.Location() *time.Location

time.Date函数会根据传入的组成部分创建一个时间值:

d := time.Date(2020, 3, 31, 15, 30, 0, 0, time.UTC)
fmt.Println(d)
// 2020-03-31 15:30:00 +0000 UTC
1
2
3

输出结果会进行规范化处理,如下所示:

d := time.Date(2020, 3, 0, 15, 30, 0, 0, time.UTC)
fmt.Println(d)
// 2020-02-29 15:30:00 +0000 UTC
1
2
3

由于月份中的日期是从1开始计数的,创建一个日期时如果将日期设为0,结果会是上一个月的最后一天。

一旦有了一个time.Time类型的值,就可以获取它的各个组成部分:

d := time.Date(2020, 3, 0, 15, 30, 0, 0, time.UTC)
fmt.Println(d.Day())
// 29
1
2
3

同样,time.Date函数会对日期值进行规范化处理,所以d.Day()会返回29。

# 日期/时间运算

在处理诸如以下的问题时,日期/时间运算是必要的:

  • 完成一项操作花了多长时间?
  • 5分钟后是几点?
  • 距离下个月还有多少天?

本方法展示了如何使用time包来回答这些问题。

# 操作方法……

  • 要计算两个时间实例之间经过的时间,可使用Time.Sub方法进行相减。
  • 要计算从现在到某个未来时间的持续时间,可使用time.Until(laterTime)。
  • 要计算从某个给定时间开始已经过去的时间,可使用time.Since(beforeTime)。
  • 要计算经过一定持续时间后的时间,可使用Time.Add方法。使用负的持续时间可以计算在某个持续时间之前的时间。
  • 要对某个时间增加/减少年、月或日,可使用Time.AddDate方法。
  • 要比较两个time.Time类型的值,可使用以下方法:
    • Time.Equal:用于检查两个时间值是否表示同一个时间实例。
    • Time.Before或Time.After:用于检查一个时间值是在给定时间值之前还是之后。

# 工作原理……

time.Duration类型表示两个时间实例之间经过的时间,以纳秒为单位,用int64类型的值表示。换句话说,如果你用一个time.Time类型的值减去另一个,就会得到一个time.Duration类型的值:

dur := tm1.Sub(tm2)
1

由于Duration是一个表示纳秒数的int64类型,所以可以进行持续时间的运算:

// 给持续时间加上1天
dur += time.Hour * 24
1
2

注意,前面代码中的最后一个操作还涉及乘法运算,因为time.Hour本身就是time.Duration类型。

可以将一个持续时间值加到一个time.Time类型的值上:

now := time.Now()
then := now.Add(dur)
1
2
提示
Duration是int64类型,这意味着一个time.Duration类型的值大约限制在290年左右。对于大多数实际情况来说,这应该足够了。然而,如果对你来说不够,你需要自己构建一个解决方案,或者找一个第三方库。

可以通过加上一个负的持续时间值,从一个time.Time类型的值中减去这个持续时间:

fmt.Println(then.Add(-dur).Equal(now))
1

注意这里使用了Time.Equal方法。这个方法在比较两个时间实例时会考虑它们的时区,而时区可能是不同的。例如,对于2024年1月9日09:00 MST(山地标准时间)和2024年1月9日08:00 PST(太平洋标准时间),Time.Equal会返回true。

使用Time.Before和Time.After来比较时间值。例如,你可以通过以下方式检查一个有过期日期的对象是否已经过期:

if object.Expiration.After(time.Now()) {
    // Object expired
}
1
2
3

也可以对给定的日期增加/减少年/月/日:

t := time.Now()
// 从现在减去1年,得到去年此时
lastYear := t.AddDate(-1, 0, 0)
// 增加1天,得到明天同一时间
tomorrow := t.AddDate(0, 0, 1)
// 增加1个月,得到下个月
nextMonth := t.AddDate(0, 1, 0)
1
2
3
4
5
6
7

这些操作的结果会进行规范化处理。例如,如果你从2020年2月29日减去1年,会得到2019年3月1日。当你处理月末的日期并且需要增加/减少月份值时,这可能会导致问题。对2020年3月31日增加一个月两次,结果会是2020年6月1日,但直接增加两个月,结果会是2020年5月31日:

d := time.Date(2020, 3, 31, 0, 0, 0, 0, time.UTC)
fmt.Println(d.AddDate(0, 1, 0).AddDate(0, 1, 0))
// 2020-06-01 00:00:00 +0000 UTC
fmt.Println(d.AddDate(0, 2, 0))
// 2020-05-31 00:00:00 +0000 UTC
1
2
3
4
5

# 日期/时间的格式化与解析

Go语言使用一种有趣且有些争议的日期/时间格式化方案。日期/时间格式通过一个特定的时间点来表示,并进行了调整,使得日期/时间的每个组成部分都是一个唯一的数字:

  • 1表示月份:“Jan”“January”“01”“1”
  • 2表示月份中的日期:“2”“_2”“02”
  • 3表示12小时制的小时数:“15”“3”“03”
  • 15表示24小时制的小时数
  • 4表示分钟数:“4”“04”
  • 5表示秒数:“5”“05”
  • 6表示年份:“2006”“06”
  • MST表示时区:“-0700”“-07:00”“-07”“-070000”“-07:00:00”“MST”
  • 0表示用0填充的毫秒数:“0”“000”
  • 9表示未填充的毫秒数:“9”“999”

# 操作方法……

  • 使用time.Parse并传入合适的格式来解析日期/时间。格式中未指定的日期/时间部分将初始化为其零值,月份的零值是1月,年份的零值是1,月份中日期的零值是1,其他部分的零值是0。如果缺少时区信息,解析后的日期/时间将是UTC时间。
  • 使用time.ParseInLocation在给定的地点解析日期/时间。时区将根据日期值和地点来确定。
  • 使用Format()方法来格式化一个日期/时间值。
func main() {
    t := time.Date(2024, 3, 8, 18, 2, 13, 500, time.UTC)
    fmt.Println("Date in yyyy/mm/dd format", t.Format("2006/01/02"))
    // Date in yyyy/mm/dd format 2024/03/08
    fmt.Println("Date in yyyy/m/d format", t.Format("2006/1/2"))
    // Date in yyyy/m/d format 2024/3/8
    fmt.Println("Date in yy/m/d format", t.Format("06/1/2"))
    // Date in yy/m/d format 24/3/8
    fmt.Println("Time in hh:mm format (12 hr)", t.Format("03:04"))
    // Time in hh:mm format (12 hr) 06:02
    fmt.Println("Time in hh:m format (24 hr)", t.Format("15:4"))
    // Time in hh:m format (24 hr) 18:2
    fmt.Println("Date-time with time zone", t.Format("2006-01-02 13:04:05 -07:00"))
    // Date-time with time zone 2024-03-08 18:02:13 +00:00
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

时区会因地点和日期的不同而变化。在下面的示例中,尽管使用相同的地点来解析日期,但时区发生了变化,因为7月9日是山地夏令时,而1月9日是山地标准时间:

loc, _ := time.LoadLocation("America/Denver")
const format = "Jan 2, 2006 at 3:04pm"
str, _ := time.ParseInLocation(format, "Jul 9, 2012 at 5:02am", loc)
fmt.Println(str)
// 2012-07-09 05:02:00 -0600 MDT
str, _ = time.ParseInLocation(format, "Jan 9, 2012 at 5:02am", loc)
fmt.Println(str)
// 2012-01-09 05:02:00 -0700 MST
1
2
3
4
5
6
7
8

# 处理时区

Go语言的time.Time类型的值包含time.Location,它可以是以下两种情况之一:

  • 一个地区标识,如“America/Denver”。在这种情况下,实际的时区取决于时间值。对于丹佛,根据实际时间值,时区可能是MDT(山地夏令时)或MST(山地标准时间)。
  • 一个固定的时区,表示偏移量。

有些应用程序处理本地时间(local time)。本地时间是在特定地点获取的日期/时间值,并且在各地都被解释为相同的值,而不是被解释为同一个时间点。生日(以及由此计算的年龄)通常是按照本地时间来解释的。也就是说,如果你出生于2005年7月14日,在2007年7月14日00:00,在纽约(东部时区)你会被认为2岁,但在同一时刻,在洛杉矶(太平洋时区),时间是2007年7月13日21:00,你仍被认为1岁。

# 操作方法……

如果你处理的是时间点,始终要捕获带有相关地点信息的日期/时间值。这样的日期/时间值可以很容易地转换到其他时区。

如果你在多个时区处理本地时间,可以在新的地点或时区重新创建time.Time类型的值来进行转换。

# 它的工作原理……

当你创建一个time.Time类型的变量时,它总是与一个地点(时区)相关联:

// 使用本地时区创建一个新时间
t := time.Date(2021,12,31,15,0,0,0, time.Local)
// 2021-12-31 15:00:00 -0700 MST
1
2
3

一旦你有了一个time.Time类型的变量,你可以获取同一时刻在不同时区的表示:

utcTime := t.In(time.UTC)
fmt.Println(utcTime)
// 2021-12-31 22:00:00 +0000 UTC
ny,err:=time.LoadLocation("America/New_York")
if err != nil {
	panic(err)
}

nyTime := t.In(ny)
fmt.Println(nyTime)
// 2021-12-31 17:00:00 -0500 EST
1
2
3
4
5
6
7
8
9
10
11

这些是同一时刻在不同时区的不同表示形式。你还可以创建一个自定义时区:

zone30 := time.FixedZone("30min", 30)
fmt.Println(t.In(zone30))
// 2021-12-31 22:00:30 +0000 30min
1
2
3

当你处理本地时间时,你会丢弃地点和时区信息:

// 创建一个本地时间,UTC时区
t := time.Date(2021,12,31,15,0,0,0, time.UTC)
// 2021-12-31 15:00:00 +0000 UTC
1
2
3

要在纽约(时区)获取相同的时间值,可以使用以下方法:

ny,err: = time.LoadLocation("America/New_York")
if err != nil {
	panic(err)
}

nyTime := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), ny)
fmt.Println(nyTime)
// 2021-12-31 15:00:00 -0500 EST
1
2
3
4
5
6
7
8

# 存储时间信息

一个常见的问题是以可移植的方式在数据库、文件等中存储日期/时间信息,以便能够正确地解释它。

# 如何操作……

你应该首先确定确切的需求:你需要存储某个时刻(instant of time)还是一天中的某个时间(time of day)?

  • 要存储某个时刻,可以执行以下操作之一:
    • 以所需的粒度存储Unix时间(即,time.Unix用于秒,time.UnixMilli用于毫秒等)。
    • 存储协调世界时(UTC,time.UTC())。
  • 要存储一天中的某个时间,可以存储time.Duration类型的值,该值表示当天中的某个时刻。以下函数将当天中的某个时刻计算为time.Duration类型的值:
func GetTimeOfDay(t time.Time) time.Duration {
    beginningOfDay: = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
    return t.Sub(beginningOfDay)
}
1
2
3
4
  • 要存储日期值,可以清除time.Time类型变量的时间部分:
date: = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
1

请注意,以这种方式存储的日期进行比较可能会有问题,因为在不同的时区,每一天会被解释为不同的时刻。

# 定时器(Timers)

使用time.Timer安排在未来某个时间执行某些任务。当定时器到期时,你将从一个通道接收到一个信号。你可以使用定时器稍后运行一个函数,或者取消运行时间过长的进程。

# 如何操作……

你可以通过以下两种方式之一创建定时器:

  • 使用time.NewTimer或time.After。定时器到期时会通过通道发送一个信号。使用select语句,或从通道读取以接收定时器到期信号。
  • 使用time.AfterFunc在定时器到期时调用一个函数。

# 它的工作原理……

time.Timer类型的定时器通过time.Duration类型的值来创建:

// 创建一个10秒的定时器
timer := time.NewTimer(time.Second*10)
1
2

定时器包含一个通道,10秒过后,该通道将接收到当前时间戳。定时器创建时通道容量为1,因此定时器运行时总能向该通道写入数据并停止定时器。换句话说,如果你没有从定时器读取数据,它不会泄漏;最终它会被垃圾回收。

定时器可用于停止长时间运行的进程:

func longProcess() {
    timer := time.NewTimer(time.Second*10)
    for {
        processData()
        
        select {
            case <-timer.C:
            // 2秒过去了
            return
        
            default:
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

以下示例展示了如何使用定时器限制函数返回所需的时间。如果计算在一秒内完成,则返回响应。如果计算时间较长,该函数会返回一个通道,调用者可以使用该通道接收结果。这个函数还展示了如何停止定时器:

func longComputation() (concurrent chan Result, result Result) {
    timer: = time.NewTimer(time.Second)
    concurrent = make(chan Result)
    
    // 启动并发计算。其结果将从通道接收
    go func() {
        concurrent <- processData()
    }()

    // 等待结果可用,或定时器到期
    select {
        case result:=<-concurrent:
            // 结果很快可用。停止定时器并返回结果。
            timer.Stop()
        	return nil,result
        
        case <-timer.C:
            // 定时器在计算出结果之前到期。返回通道
            return concurrent,Result{}
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

请注意,定时器可能在调用timer.Stop()之前刚刚到期。这没关系。定时器最终会到期并被垃圾回收。调用timer.Stop()只是为了防止定时器在不必要的情况下继续保持活动状态。

提示
当另一个goroutine正在从定时器读取数据时,你不能并发调用Timer.Stop。因此,如果你必须调用Timer.Stop,请从监听定时器通道的同一个goroutine中调用它。

使用time.After也可以实现同样的功能:

concurrent = make(chan Result)

// 启动并发计算。其结果将从通道接收
go func() {
	concurrent <- processData()
}()

select {
    case result:=<-concurrent:
    	return nil,result
    
    case <-time.After(time.Second):
    	return concurrent,Result{}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 时间 ticker(Tickers)

使用time.Ticker定期执行一项任务。你将定期通过通道接收一个信号。与time.Timer不同,你必须注意如何处理time.Ticker。如果你忘记停止time.Ticker,当它超出作用域时,它不会被垃圾回收,这会导致泄漏。

# 如何操作……

  1. 使用time.Ticker创建一个新的time.Ticker。
  2. 从time.Ticker的通道读取数据,以接收定期的“tick”信号。
  3. 当你使用完time.Ticker后,停止它。你不需要清空time.Ticker的通道。

# 它的工作原理……

time.Ticker用于周期性事件。常见的模式如下:

func poorMansClock(done chan struct{}) {
    // 创建一个周期为1秒的新time.Ticker
    ticker: = time.NewTicker(time.Second)
    // 一旦完成,停止time.Ticker
    defer ticker.Stop()
    
    for {
        select {
            case <-done:
            	return
            
            case <-ticker.C:
            	fmt.Println(time.Now())
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如果你错过了“tick”信号会发生什么?如果你运行一个长时间的进程,导致你无法监听time.Ticker的通道,就可能出现这种情况。当你再次开始监听时,time.Ticker会发送大量的“tick”信号吗?

与time.Timer类似,time.Ticker使用容量为1的通道。因此,如果你不从通道读取数据,它最多只能存储一个“tick”信号。当你再次开始从通道监听时,你会立即收到错过的“tick”信号,当下一个周期到期时,你会收到下一个“tick”信号。例如,考虑以下程序,它每秒调用一次给定的函数:

func everySecond(f func(), done chan struct{}) {
    // 创建一个周期为1秒的新`time.Ticker`
    ticker:=time.NewTicker(time.Second)
    start:=time.Now()
    // 一旦完成,停止`time.Ticker`
    defer ticker.Stop()
    
    for {
        select {
            case <-done:
            	return
            
            case <-ticker.C:
                fmt.Println(time.Since(start).Milliseconds())
                // 调用函数
                f()
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

假设对f()的第一次调用运行了10毫秒,但第二次调用运行了1.5秒。在f()运行时,没有人从time.Ticker的通道读取数据,因此会错过一个“tick”信号。一旦f()返回,select语句将立即读取这个错过的“tick”信号,500毫秒后,它将收到下一个“tick”信号。输出如下:

1000
2000
3500
4000
5000
1
2
3
4
5

**提示:**与time.Timer不同,你可以在从time.Ticker的通道读取数据时并发停止它。

第2章 字符串处理
第4章 使用数组、切片和映射

← 第2章 字符串处理 第4章 使用数组、切片和映射→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式