《Go Cookbook CN》系列 11:时间处理实战——时间与日期操作全解析
本文聚焦Go语言标准库中time包的核心使用方法,从计算机时钟类型的底层认知出发,系统拆解Go中时间与日期的获取、算术运算、表示形式、时区处理、时间段操作、程序暂停、耗时测量、格式转换与解析等全场景实操能力。学完本文,你将彻底掌握Go语言中时间处理的核心逻辑与避坑要点,能从容应对业务开发中各类时间相关的场景需求。
本篇核心收获
- 理解计算机挂钟与单调时钟的核心差异,掌握Go中两类时钟的正确使用场景
- 熟练使用
time包完成时间的获取、加减运算、日期/时区的表示与转换 - 掌握
Duration类型的定义、运算及不同单位的转换方法 - 学会用单调时钟精准测量程序耗时,规避挂钟计时的坑点
- 精通时间的格式化输出与字符串解析,掌握Go特有的布局模式规则及时区解析避坑技巧
11.0 计算机时钟核心认知
时间与日期处理是所有编程语言的核心能力,Go通过time包提供完整的时间操作体系,而理解计算机的两类时钟是正确使用time包的基础:
- 挂钟:对应现实世界的时间,会与网络时间服务器同步,可能被用户/程序调整,适合「报时」场景,但不适合测量时间差(可能出现负数)
- 单调时钟:始终向前递增,不受时间调整影响,适合「计时/测量耗时」场景
核心原则:挂钟用于报时,单调时钟用于计时。
模块小结:本模块核心是区分挂钟与单调时钟的差异及适用场景,这是后续时间操作避坑的基础。
11.1 获取当前时间(报时)
11.1.1 核心方法
使用time.Now()函数可返回当前时间,该函数返回Time结构体实例,同时包含挂钟和单调时钟的读数:
time.Now()模块小结:
time.Now()是获取当前时间的核心入口,返回的Time结构体是Go时间处理的核心载体。
11.2 时间的算术运算
11.2.1 时间加减(Add方法)
Time结构体的Add方法接收Duration类型参数,实现时间的加减:
- 加时间:为当前时间添加指定时间段
t0 := time.Now() // 获取当前时间
t1 := t0.Add(10 * time.Minute) // 为时间 t0 加 10 分钟- 减时间:传入负数的
Duration即可实现时间减法
t2 := t0.Add(-10 * time.Minute) // 为时间 t0 减去 10 分钟11.2.2 时间差计算(Sub方法)
Sub方法用于计算两个Time结构体的时间差,返回Duration类型:
t3 := t1.Sub(t2) // 计算 t1 与 t2 的时间差,返回 Duration 类型模块小结:
Add方法负责时间的加减运算(减法通过负数实现),Sub方法负责两个时间的差值计算,二者是时间算术运算的核心。
11.3 日期的表示与创建
11.3.1 核心说明
Go标准库未提供专门的Date结构体,所有日期相关操作均基于Time结构体实现。
11.3.2 自定义日期创建(Date函数)
使用time.Date函数可创建指定的日期时间,返回Time结构体,函数参数需按「年、月、日、时、分、秒、纳秒、时区」顺序传入,其中时区参数不可为nil(否则触发panic):
t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)11.3.3 日期信息提取
- 提取月份名称:
Month方法返回Month类型,通过String方法获取月份名称
m := t.Month() // 返回值为 Month 类型
m.String() // 提取月份的名称:"November"- 提取星期几:
Weekday方法返回Weekday类型,通过String方法获取星期名称
w := t.Weekday() // 返回值为 Weekday 类型
w.String() // 提取日期的名称(即星期几):"Tuesday"模块小结:Go通过
Time结构体承载日期信息,Date函数是创建自定义日期的核心,需注意时区参数的非空约束,同时可通过Month/Weekday方法提取日期维度的关键信息。
11.4 时区的表示与转换
11.4.1 时区的核心载体(Location结构体)
Go中用Location结构体表示时区,所有时区均以UTC为基准定义偏移量(范围UTC-12:00至UTC+14:00),底层依赖IANA的tz/zoneinfo数据库(命名格式:Area/Location,如Asia/Singapore)。
11.4.2 Location的创建方式
方式1:LoadLocation(加载系统tz数据库)
从本地计算机的tz数据库加载时区信息,返回对应Location:
func main() {
location, err := time.LoadLocation("Asia/Singapore") // 加载时区信息
if err != nil {
log.Println("Cannot load location:", err)
}
fmt.Println("location:", location)
utcTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
fmt.Println("UTC time:", utcTime)
fmt.Println("equivalent in Singapore:", utcTime.In(location))
}输出结果:
location: Asia/Singapore
UTC time: 2009-11-10 23:00:00 +0000 UTC
equivalent in Singapore: 2009-11-11 07:00:00 +0800 +08注意:
LoadLocation仅加载时区偏移量,时区名称显示为偏移量(如+08),若需自定义时区名称,需使用LoadLocationFromTZData。
方式2:FixedZone(自定义时区)
创建自定义时区(无需依赖tz数据库),可自定义时区名称和偏移量(秒数):
func main() {
location := time.FixedZone("Singapore Time", 8*60*60)
fmt.Println("location:", location)
utcTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
fmt.Println("UTC time:", utcTime)
fmt.Println("equivalent in Singapore:", utcTime.In(location))
}输出结果:
location: Singapore Time
UTC time: 2009-11-10 23:00:00 +0000 UTC
equivalent in Singapore: 2009-11-11 07:00:00 +0800 Singapore Time模块小结:
Location是Go表示时区的核心,LoadLocation适用于加载系统内置时区,FixedZone适用于自定义时区,二者均可通过Time.In方法完成不同时区的时间转换。
11.5 时间段(Duration)的表示与操作
11.5.1 Duration的本质
Duration是int64类型的封装,用于表示时间段,单位为纳秒,支持各类时间单位的运算。
11.5.2 Duration的创建与转换
1. 基础创建
d := 2 * time.Hour // 2 小时
// 多单位组合创建
d := (2*time.Hour) + (34*time.Minute) + (5*time.Second) // 2小时34分5秒2. 字符串转换与单位转换
- 转换为字符串:直接打印或调用
String方法
fmt.Println(d) // 输出:2h0m0s(以2小时为例)- 转换为指定单位:通过
Minutes()/Seconds()/Milliseconds()等方法实现
fmt.Println(d.Minutes()) // 2小时34分5秒转换为分钟:154.08333333333332注意:
Duration仅提供到小时及以下单位的转换方法,无天/周等更大单位的内置方法。
模块小结:
Duration是Go表示时间段的核心类型,本质为int64,支持多单位组合创建和精准的单位转换,需注意大单位转换需自行计算。
11.6 程序暂停(time.Sleep)
11.6.1 核心用法
time.Sleep接收Duration参数,使当前goroutine暂停指定时间段(main goroutine暂停则程序整体暂停,其他goroutine不受影响):
time.Sleep(2 * time.Minute) // 暂停 2 分钟模块小结:
time.Sleep是实现程序/goroutine暂停的核心函数,仅影响当前goroutine,适用于等待事件、模拟耗时等场景。
11.7 精准测量程序耗时(单调时钟的应用)
11.7.1 核心原理
Time结构体包含单调时钟数据(格式:m=±<value>,表示程序启动后的累计时间),仅time.Now()创建的Time包含该数据;- 挂钟精度不足且可能被调整,单调时钟始终递增,是测量耗时的唯一可靠方式。
11.7.2 耗时测量方法
func main() {
// 模拟程序耗时操作
time.Sleep(10 * time.Second)
// 记录两个时间点
t1 := time.Now() // 包含单调时钟数据
t2 := time.Now()
fmt.Println("t1:", t1)
fmt.Println("t2:", t2)
fmt.Println("difference:", t2.Sub(t1)) // 计算差值,精准获取耗时
}输出示例:
t1: 2021-10-09 15:12:12.432516 +0800 +08 m=+10.005330678
t2: 2021-10-09 15:12:12.432516 +0800 +08 m=+10.005330984
difference: 306ns11.7.3 避坑指南
- 部分方法会剥离单调时钟数据:
AddDate/Round/Truncate/In/Local/UTC等方法返回的Time无单调时钟; - 手动剥离单调时钟:
time.Now().Round(0)可删除单调时钟数据; - 无单调时钟的
Time调用Sub:会基于挂钟计算,精度极低(短时间差可能为0)。
示例(无单调时钟的耗时测量):
func main() {
t1 := time.Now().Round(0) // 删除 t1 的单调时钟数据
t2 := time.Now().Round(0) // 删除 t2 的单调时钟数据
fmt.Println("t1:", t1)
fmt.Println("t2:", t2)
fmt.Println("difference:", t2.Sub(t1)) // 输出0s(挂钟精度不足)
}模块小结:测量程序耗时必须依赖
time.Now()返回的含单调时钟的Time结构体,避免使用剥离单调时钟的Time实例,否则会导致测量结果不准确。
11.8 时间的格式化输出
11.8.1 格式化核心标准
Go支持主流的时间格式化标准,核心包括:
| 标准 | 适用场景 | 核心特点 |
|---|---|---|
| ISO 8601 | 通用行业/政府规范 | 支持基本/扩展格式、周日期 |
| RFC 3339 | 互联网场景 | ISO子集,强制连字符,无周日期 |
| RFC 822/850 | 邮件/USENET消息 | 早期互联网标准,含时区缩写 |
| RFC 1123 | Internet主机规范 | 基于RFC 822,适配主机场景 |
11.8.2 Go的格式化核心规则(布局模式)
Time.Format方法接收「布局字符串」作为参数,按布局格式输出时间,核心规则:
- 布局中的数字是固定标识,非占位符:月=1、日=2、小时=3、分钟=4、秒=5、年=6、时区=7;
- 布局遵循美国惯例(月在前、日在后),如
01/02表示「1月2日」。
11.8.3 格式化实操
1. 自定义布局格式化
func main() {
t := time.Now()
fmt.Println(t.Format("3:04PM")) // 格式:小时:分钟AM/PM
fmt.Println(t.Format("Jan 02, 2006")) // 格式:月份缩写 日期, 年份
}输出示例:
1:45PM
Oct 23, 20212. 预定义布局(推荐)
time包内置了标准布局常量,可直接使用:
func main() {
t := time.Now()
fmt.Println(t.Format(time.UnixDate))
fmt.Println(t.Format(time.RFC822))
fmt.Println(t.Format(time.RFC850))
fmt.Println(t.Format(time.RFC1123))
fmt.Println(t.Format(time.RFC3339))
}输出示例:
Sat Oct 23 15:05:37 +08 2021
22 Oct 23 15:05 +08
Saturday, 23-Oct-21 15:05:37 +08
Sat, 23 Oct 2021 15:05:37 +08
2021-10-23T15:05:37+08:003. 特殊布局
func main() {
t := time.Now()
fmt.Println(t.Format(time.Kitchen)) // 厨房格式:3:04PM
fmt.Println(t.Format(time.Stamp)) // 时间戳:Oct 23 15:10:53
fmt.Println(t.Format(time.StampMilli)) // 毫秒级时间戳
fmt.Println(t.Format(time.StampMicro)) // 微秒级时间戳
fmt.Println(t.Format(time.StampNano)) // 纳秒级时间戳
}输出示例:
3:10PM
Oct 23 15:10:53
Oct 23 15:10:53.899
Oct 23 15:10:53.899873
Oct 23 15:10:53.89987300011.8.4 避坑指南
- 布局数字错误会导致格式化结果错误,且无报错:如用
3:09pm替代3:04pm,分钟会显示为09而非实际值; - 布局中的数字是固定标识,必须严格对应(月=1、日=2等),不可随意替换。
模块小结:Go通过「固定布局模式」实现时间格式化,优先使用内置标准布局常量,避免自定义布局的数字错误,核心是牢记布局中数字的固定含义。
11.9 时间字符串解析为Time结构体
11.9.1 核心方法(time.Parse)
time.Parse接收「布局字符串」和「时间字符串」,将字符串转换为Time结构体,布局规则与Format方法完全一致:
func main() {
str := "4:31am +0800 on Oct 1, 2021" // 待解析的时间字符串
layout := "3:04pm -0700 on Jan 2, 2006" // 定义时间格式化布局
t, err := time.Parse(layout, str)
if err != nil {
log.Println("Cannot parse:", err)
}
fmt.Println(t.Format(time.RFC3339)) // 用预定义的布局格式化解析结果
}输出结果:
2021-10-01T04:31:00+08:0011.9.2 解析常见错误场景
场景1:时间字符串与布局不匹配
若时间字符串包含布局外的额外内容,或缺少布局要求的内容,会返回错误:
// 布局缺少日期部分,时间字符串含日期
str := "4:31am +0800 on Oct 1, 2021"
layout := "3:04pm -0700"
t, err := time.Parse(layout, str) // 报错:extra text: " on Oct 1, 2021"场景2:布局数字错误
布局中使用错误数字(如分钟用09而非04),会导致解析失败:
layout := "3:09pm -0700 on Jan 2, 2006"
t, err := time.Parse(layout, str) // 报错:cannot parse "31am +0800 on Oct 1, 2021" as "<09"场景3:时区缩写解析坑点
使用时区缩写(如SGT/EST)解析时,Parse会将其识别为虚构时区(偏移量为0),导致时区偏移错误:
str := "4:31am SGT on Oct 1, 2021"
layout := "3:04pm MST on Jan 2, 2006"
t, err := time.Parse(layout, str)
// 输出:01 Oct 21 04:31 +0000(偏移量错误,应为+0800)
fmt.Println(t.Format(time.RFC822Z))避坑方案:解析时使用数字偏移量(如+0800)而非时区缩写。
模块小结:
time.Parse是字符串转Time的核心,需保证布局与时间字符串完全匹配,避免布局数字错误,时区解析优先使用数字偏移量,规避缩写导致的偏移错误。
本篇核心知识点速记
- 时钟类型:挂钟(报时)、单调时钟(计时),
time.Now()返回的Time包含两类时钟数据; - 时间运算:
Add(加减,减法传负数)、Sub(计算差值)是核心方法; - 日期表示:无专用
Date结构体,通过Time承载,Date函数创建自定义日期(时区参数非空); - 时区处理:
Location表示时区,LoadLocation加载系统时区,FixedZone自定义时区; - Duration:
int64封装,表示时间段,支持多单位组合与转换(无天/周等大单位方法); - 程序暂停:
time.Sleep使当前goroutine暂停指定Duration; - 耗时测量:依赖
Time的单调时钟,避免使用剥离单调时钟的Time实例; - 时间格式化:
Format方法基于固定布局(数字为固定标识),优先使用内置标准布局; - 时间解析:
Parse方法与Format布局规则一致,时区解析优先用数字偏移量,规避缩写坑点。
