Menu

Golang time 包:Now、Sleep、Format、Parse 与 Duration

如何在 Go 中处理日期和时间:time.Now 与 time.Sleep、Duration 运算、Format 和 Parse 使用的 2006-01-02 15:04:05 参考布局、时区、Unix 时间戳,以及定时器和 Ticker。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

一个程序看懂基础用法

time.Now() 返回当前的本地时间,time.Sleep 暂停当前 goroutine,time.Since 测量经过的时间,time.Date 构造一个特定的时刻。本页其余部分会逐一讲解。示例使用固定日期和 UTC,所以无论在哪里运行,输出都相同。

Sleep 与 Duration

time.Duration 是一个表示纳秒数的 int64。这个包定义了一些常量,用来写出易读的值:

常量值
time.Nanosecond1
time.Microsecond1000 ns
time.Millisecond1000 µs
time.Second1000 ms
time.Minute60 s
time.Hour60 min

没有 time.Day,因为在时区因夏令时调整的那天,一天并不总是 24 小时。日历上的天数请用 AddDate(见下文)。

Duration 是整数,由此产生两个常见 bug:

  • time.Sleep(5) 只休眠 5 纳秒。一定要乘上单位:time.Sleep(5 * time.Second)。
  • 当 n 是 int 变量时,time.Sleep(n * time.Second) 无法编译。需要转换:time.Duration(n) * time.Second。像 5 这样的常量不需要转换就能用,因为无类型常量会自动适配 Duration 类型。

ParseDuration 接受 ns、us(或 µs)、ms、s、m 和 h,可以组合使用,例如 "2h45m" 或 "1.5s"。它没有表示天的单位。

格式化:2006-01-02 参考布局

Go 不使用 %Y-%m-%d 或 yyyy-MM-dd。布局就是把参考时间

Mon Jan 2 15:04:05 MST 2006

写成你想要的输出样子。这些值经过挑选,每一个都独一无二:月 1、日 2、时 3(或 15)、分 4、秒 5、年 6(2006)、时区偏移 -7(-0700)。可以把它读作 01/02 03:04:05PM '06 -0700。

最常用的布局标记:

标记含义示例
2006 / 06年,4 位或 2 位2026 / 26
01 / 1 / Jan / January月03 / 3 / Mar / March
02 / 2 / _2日(补零、不补、补空格)05 / 5 / " 5"
Mon / Monday星期Thu / Thursday
15时,24 小时制09
03 / 3时,12 小时制09 / 9
04 / 4分07 / 7
05 / 5秒03 / 3
PM / pm上午或下午标记AM
.000 / .999秒的小数部分(固定位数 / 去掉末尾的零).250 / .25
MST时区缩写UTC
-0700 / -07:00数字形式的时区偏移+0000 / +00:00
Z07:00和 -07:00 一样,但 UTC 打印为 ZZ

预定义的布局:

常量布局
time.RFC33392006-01-02T15:04:05Z07:00(API 和 JSON 请用这个)
time.RFC3339Nano2006-01-02T15:04:05.999999999Z07:00
time.DateTime(Go 1.20)2006-01-02 15:04:05
time.DateOnly(Go 1.20)2006-01-02
time.TimeOnly(Go 1.20)15:04:05
time.Kitchen3:04PM
time.RFC1123Mon, 02 Jan 2006 15:04:05 MST(HTTP 日期请改用 http.TimeFormat)

经典错误是用错误的数字写布局,例如 "2023-01-01"。Go 不会拒绝它,而是原样复制不认识的字符,替换认识的字符:每个 2 都是日,3 是 12 小时制的时,两个 01 都是月,所以 3 月 5 日 9:07 会被格式化成 5059-03-03。像 "YYYY-MM-DD" 这样的布局里根本没有标记,会原样打印出来。如果格式化出来的日期看起来很奇怪,检查一下布局用的是不是参考时间里的那些值。

把字符串解析成时间

time.Parse(layout, value) 使用同样的布局,并返回一个你必须检查的错误:

如果输入中没有时区,Parse 返回 UTC 时间。同一个钟表时间字符串在不同时区可能表示不同的时刻,这就是 ParseInLocation 存在的原因。错误信息会指出哪个部分匹配失败,调试布局时很有帮助。

时区

time.Time 是一个时刻加上一个用于显示的位置。用 In 改变位置只会改变它的打印方式,而不会改变它所表示的时刻。

  • time.UTC 始终可用。存储和传输时间时用 UTC(或带偏移的 RFC 3339),只在显示时才转换。
  • time.Local 是机器所在的时区。在服务器和容器里它通常是 UTC,在笔记本电脑上则不是,所以依赖它的代码在不同地方表现不同。
  • time.LoadLocation("Europe/Berlin") 从操作系统读取 IANA 时区数据库。精简的容器镜像里往往没有这个数据库,这时调用会返回错误。空白导入 _ "time/tzdata" 会把数据库嵌入你的二进制文件(大约 450 KB),这样它总能正常工作。一定要处理这个错误。
  • time.FixedZone(name, offsetSeconds) 创建一个偏移固定的时区。它没有夏令时规则,所以适合用于你收到的偏移量,而不是有名字的地区。

时间运算与比较

输出中值得注意的地方:

  • 在 1 月 31 日上调用 AddDate(0, 1, 0) 得到的是 3 月 3 日,而不是 2 月 28 日。Go 先加一个月得到“2 月 31 日”,再把溢出的部分规范化。如果你需要“下个月同一天,超出就取月末”,请自己写这段逻辑。
  • Sub 返回一个 Duration,它的上限大约是 292 年。要计算相差的日历天数,比较两个日期在 UTC 零点的时间,再除以 24 小时。
  • 要得到一天的开始时间,用 Date() 重新构造时间。t.Truncate(24 * time.Hour) 是相对于 UTC 的零时间取整的,所以对 UTC 以外的任何时区都会得到错误的结果。

用 Equal、Before、After 比较,永远不要用 ==。 time.Now() 包含一个单调时钟读数,这样即使系统时钟被调整,time.Since 也能保持正确。== 会连同这个读数和位置一起比较,所以表示同一时刻的两个 Time 值可能不相等。出于同样的原因,不要不经规范化就把 time.Time 用作 map 的键(t.UTC().Round(0) 会去掉单调时钟读数)。

零值的 time.Time 是公元 1 年 1 月 1 日 00:00 UTC。用 t.IsZero() 检查它。

测量经过的时间

start := time.Now()
doWork()
log.Printf("doWork took %v", time.Since(start))

time.Since(start) 就是 time.Now().Sub(start),time.Until(deadline) 就是 deadline.Sub(time.Now())。两者在可用时都使用单调时钟,所以不受系统时钟变化的影响。要对代码做基准测试,请使用 testing 包中 Go 的基准测试功能,而不是手动计时。

定时器与 Ticker

time.After(d) 返回一个通道,它在 d 之后接收一次值。time.Timer 也是一样的东西,只是多了一个 Stop 方法。time.Ticker 每隔一个周期发送一个值,直到你停止它。

20、40 和 60 毫秒时的三次 tick 都远早于 110 毫秒的定时器,所以程序在第三次 tick 之后停止。time.AfterFunc(d, f) 会在 d 之后于独立的 goroutine 中运行 f,适合一次性的延迟任务。

从 Go 1.23 起,不再被引用的定时器和 Ticker 即使从未调用 Stop 也会被垃圾回收,而且已停止或已重置的定时器的通道不会再收到过期的值。用 defer 调用 Stop 仍然是表明 Ticker 生命周期已结束的清晰方式。当时间限制作用于整个操作而不是一次等待时,context.WithTimeout 通常比定时器更易读。

JSON 中的时间

在 JSON 中,time.Time 会自动与 RFC 3339 字符串相互转换,精度为纳秒:

type Event struct {
	Name string    `json:"name"`
	At   time.Time `json:"at"`
}
// {"name":"deploy","at":"2026-09-23T14:30:00Z"}

其他格式的 JSON 字符串会解析失败。对于 Unix 时间戳或自定义格式,存成 int64 或字符串再转换,或者定义一个带有自己 UnmarshalJSON 的类型。

常见错误

  • time.Sleep(1) 或 time.Sleep(n) 直接传数字。 那是纳秒。
  • 布局里的数字写错了。 "2023-01-01" 或 "YYYY-MM-DD" 都不是布局。请用 "2006-01-02"。
  • 把分钟和月份搞混。 分钟是 04,月份是 01。"15:01" 打印的是小时和月份,而不是分钟。
  • 用 == 比较。 请用 Equal。
  • 依赖 time.Local。 它在不同机器上不一样。请明确使用 time.UTC 或加载的位置。
  • 忽略 LoadLocation 或 Parse 返回的错误。 两者在真实输入上都会失败。Parse 失败时返回零值时间,打印出来是公元 1 年;LoadLocation 失败时返回 nil 位置,会让 t.In(loc) panic。

常见问题

Go 中如何让程序休眠?

用一个 time.Duration 调用 time.Sleep:time.Sleep(2 * time.Second) 或 time.Sleep(500 * time.Millisecond)。像 time.Sleep(2) 这样直接写数字也能编译,但只会休眠 2 纳秒,因为 Duration 的单位是纳秒。Sleep 只阻塞当前的 goroutine。

为什么 Go 用 2006-01-02 15:04:05 作为日期格式?

Go 用示例来格式化日期。布局就是把参考时间 Mon Jan 2 15:04:05 MST 2006 写成你想要的输出样子。它的各个部分按美国的书写顺序递增:月 1、日 2、时 3(24 小时制为 15)、分 4、秒 5、年 6(2006)、时区偏移 7(-0700)。所以 "2006-01-02" 表示年-月-日,"02/01/2006" 表示日/月/年。

Go 中如何解析日期字符串?

用 time.Parse(layout, value),布局按参考时间来写:t, err := time.Parse("2006-01-02", "2026-09-23")。一定要检查 err。如果字符串里没有时区信息,结果就是 UTC;要按其他时区解释,用 time.ParseInLocation。

Go 中如何获取 Unix 时间戳?

time.Now().Unix() 以 int64 返回自 1970 年 1 月 1 日 UTC 以来的秒数。UnixMilli()、UnixMicro() 和 UnixNano() 提供更细的单位。反过来转换时,用 time.Unix(sec, 0) 或 time.UnixMilli(ms)。

Go 中如何比较两个时间?

用 t1.Before(t2)、t1.After(t2) 和 t1.Equal(t2)。不要用 ==:它还会比较位置和单调时钟读数,所以表示同一时刻的两个值可能不相等。t2.Sub(t1) 以 Duration 的形式给出两者的差。

Coddy 编程语言插图

和 Coddy 一起学编程

开始