Skip to content

《Go Cookbook CN》系列 11:时间处理实战——时间与日期操作全解析

约 3538 字大约 12 分钟

《Go Cookbook CN》系列Go语言

2026-04-02

本文聚焦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的本质

Durationint64类型的封装,用于表示时间段,单位为纳秒,支持各类时间单位的运算。

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: 306ns

11.7.3 避坑指南

  1. 部分方法会剥离单调时钟数据:AddDate/Round/Truncate/In/Local/UTC等方法返回的Time无单调时钟;
  2. 手动剥离单调时钟:time.Now().Round(0)可删除单调时钟数据;
  3. 无单调时钟的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 1123Internet主机规范基于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, 2021

2. 预定义布局(推荐)

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:00

3. 特殊布局

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.899873000

11.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:00

11.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的核心,需保证布局与时间字符串完全匹配,避免布局数字错误,时区解析优先使用数字偏移量,规避缩写导致的偏移错误。

本篇核心知识点速记

  1. 时钟类型:挂钟(报时)、单调时钟(计时),time.Now()返回的Time包含两类时钟数据;
  2. 时间运算:Add(加减,减法传负数)、Sub(计算差值)是核心方法;
  3. 日期表示:无专用Date结构体,通过Time承载,Date函数创建自定义日期(时区参数非空);
  4. 时区处理:Location表示时区,LoadLocation加载系统时区,FixedZone自定义时区;
  5. Duration:int64封装,表示时间段,支持多单位组合与转换(无天/周等大单位方法);
  6. 程序暂停:time.Sleep使当前goroutine暂停指定Duration
  7. 耗时测量:依赖Time的单调时钟,避免使用剥离单调时钟的Time实例;
  8. 时间格式化:Format方法基于固定布局(数字为固定标识),优先使用内置标准布局;
  9. 时间解析:Parse方法与Format布局规则一致,时区解析优先用数字偏移量,规避缩写坑点。