一个程序看懂基础用法
time.Now() 返回当前的本地时间,time.Sleep 暂停当前 goroutine,time.Since 测量经过的时间,time.Date 构造一个特定的时刻。本页其余部分会逐一讲解。示例使用固定日期和 UTC,所以无论在哪里运行,输出都相同。
Sleep 与 Duration
time.Duration 是一个表示纳秒数的 int64。这个包定义了一些常量,用来写出易读的值:
| 常量 | 值 |
|---|---|
time.Nanosecond | 1 |
time.Microsecond | 1000 ns |
time.Millisecond | 1000 µs |
time.Second | 1000 ms |
time.Minute | 60 s |
time.Hour | 60 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 打印为 Z | Z |
预定义的布局:
| 常量 | 布局 |
|---|---|
time.RFC3339 | 2006-01-02T15:04:05Z07:00(API 和 JSON 请用这个) |
time.RFC3339Nano | 2006-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.Kitchen | 3:04PM |
time.RFC1123 | Mon, 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 的形式给出两者的差。