Go 与时间
time包深度剖析:Time/Duration/Location 三大核心、单调时钟、时区处理、定时器、Ticker、性能优化与陷阱
前置知识
- Go 与文件监控:建议先完成前一篇的学习
学习目标
- 掌握「1. 历史动机与背景」的核心机制、典型用法与常见陷阱
- 掌握「2. 形式化定义」的核心机制、典型用法与常见陷阱
- 掌握「3. 理论推导」的核心机制、典型用法与常见陷阱
- 掌握「4. 代码示例」的核心机制、典型用法与常见陷阱
- 掌握「5. 对比分析」的核心机制、典型用法与常见陷阱
1. 历史动机与背景
1.1 时间度量的物理学基础
时间是物理学的基本维度之一。在经典力学中,时间被视为绝对流逝;而在相对论中,时间是时空的一部分,随观察者参考系而变。对于计算机系统而言,时间通常分为两类:
- 天文时间(Solar Time / UT1):基于地球自转,存在不均匀性(地球转速受潮汐摩擦影响减慢)。
- 原子时间(International Atomic Time, TAI):基于铯原子振荡,1967 年起被定义为 SI 秒,高度均匀。
由于两者存在差异,引入了 UTC(协调世界时),通过插入闰秒(leap second)保持与 UT1 的差异不超过 0.9 秒。闰秒是分布式系统的噩梦:2012 年 6 月 30 日的闰秒曾导致 Java、Linux、MySQL 多个服务异常。
1.2 操作系统时钟
Linux 内核提供两种主要时钟:
| 时钟 | 名称 | 特性 | 系统调用 |
|---|---|---|---|
CLOCK_REALTIME | 墙上时钟 | 可被 NTP 修改,可能跳变 | gettimeofday、clock_gettime |
CLOCK_MONOTONIC | 单调时钟 | 单调递增,不受 NTP 影响 | clock_gettime |
CLOCK_BOOTTIME | 启动时钟 | 含休眠时间 | clock_gettime |
CLOCK_PROCESS_CPUTIME_ID | 进程 CPU 时间 | 进程实际消耗 CPU | clock_gettime |
1.3 Go time 包的设计动机
Go 在 1.0(2012 年)引入 time 包时仅有 wall clock。但在分布式系统场景中暴露了若干问题:
- NTP 跳变导致计时错误:两次
time.Now()的差值可能为负数。 - 虚拟机迁移导致时钟跳变:云环境中虚拟机热迁移会引起时钟跳变。
- 闰秒插入导致服务异常:2015 年 7 月 1 日的闰秒曾使大量 Go 服务挂死。
为此,Go 1.9(2017 年 8 月)引入 单调时钟(monotonic clock),在 time.Time 中同时存储 wall clock 与 monotonic clock 两个值。time.Since 与 time.Until 优先使用单调时钟,避免 NTP 影响。
1.4 IEEE 1588 与分布式时间同步
在金融交易、工业控制等高精度场景中,NTP(精度毫秒级)不足以满足需求。IEEE 1588 PTP(Precision Time Protocol)可将网络中多个节点的时钟同步到亚微秒级。Go 标准库未直接支持 PTP,但可通过 github.com/beevik/ntp 等第三方库访问 NTP 服务器。
2. 形式化定义
2.1 Time 的代数结构
设 为时间值集合, 为 Duration 集合, 为 Location 集合。time.Time 可形式化为三元组:
其中:
- 为 wall clock,自 0001-01-01 起的纳秒数(仅 63 位有效)。
- 为 monotonic clock,自进程启动起的纳秒数(仅当存在时有效)。
- 为时区信息。
2.2 Duration 的形式化
time.Duration 是 int64 纳秒值:
其取值范围为 纳秒,约 年。常用单位换算:
2.3 单调时钟的不变量
单调时钟满足以下不变量:
但单调时钟的绝对值无意义(不同进程间不可比较),仅差值有意义:
2.4 时区的形式化
时区可视为 UTC 偏移函数:
对于固定偏移时区(如 UTC+8),偏移为常量;对于含夏令时的时区(如 America/New_York),偏移随时间变化。
IANA 时区数据库(tzdata)将全球时区分为约 400 个区域,每个区域有完整的偏移历史与夏令时规则。
3. 理论推导
3.1 时间精度的下界
现代 x86-64 CPU 的 RDTSC 指令读取时间戳计数器,精度可达纳秒级。但 clock_gettime 系统调用本身有开销:
| 操作 | 开销(ns) |
|---|---|
clock_gettime(CLOCK_MONOTONIC) | ~20-30 ns |
time.Now() (Go) | ~30-40 ns |
time.Since() (Go) | ~5 ns(仅减法) |
Go time.Now() 通过 vDSO(虚拟动态共享对象)避免陷入内核态,开销较低。
3.2 单调时钟的语义推导
为什么 Go 选择同时存储 wall clock 与 monotonic clock?设两次 time.Now() 得到 :
- 若仅用 wall clock:,但 NTP 可能在 之间调整时钟,导致 或 异常大。
- 若仅用 monotonic clock: 准确,但无法表示绝对时刻(如”今天 14:00”)。
Go 的折中方案:
注意:time.Now() 总是包含单调时钟,但 time.Parse、time.Unix、time.Date 构造的 Time 不包含单调时钟。
3.3 Timer 的内存模型
time.Timer 与 time.Ticker 的实现基于 Go runtime 的最小堆(min-heap)。每个 P(处理器)维护独立的 timer 堆,避免全局锁。
Timer 的触发流程:
- 创建 Timer 时,runtime 将其插入当前 P 的堆中。
- runtime 调度器在调度循环中检查堆顶,若到期则触发回调。
Timer.C收到一个Time值。
复杂度:
- 创建 Timer:(堆插入)。
- 停止 Timer:(堆删除)。
- 触发 Timer:(堆顶弹出)。
3.4 时区数据库的内存占用
完整加载 IANA tzdata(约 450 个时区)大约消耗 1-2 MB 内存。Go 默认从系统读取 tzdata(Linux 在 /usr/share/zoneinfo,Windows 需单独安装)。
Go 1.15+ 支持通过 _ "time/tzdata" 包内嵌 tzdata,便于生成不依赖系统的二进制:
import _ "time/tzdata" // 将 tzdata 嵌入二进制,约 450KB
3.5 复杂度分析
| 操作 | 时间复杂度 | 备注 |
|---|---|---|
time.Now() | vDSO 调用 | |
time.Sleep(d) | 创建并等待 timer | |
time.After(d) | 创建 timer + channel | |
Timer.Stop() | 堆删除 | |
Timer.Reset(d) | 堆调整 | |
Time.Format() | 为格式串长度 | |
Time.Parse() | 为输入长度 | |
Time.Add() | 简单算术 | |
LoadLocation(name) | (缓存) | 首次 解析 tzdata |
4. 代码示例
4.1 基础:获取与格式化时间
// Package main 演示 time 包最基础的时间获取与格式化
package main
import (
"fmt"
"time"
)
func main() {
// 获取当前时间(包含 wall clock 与 monotonic clock)
now := time.Now()
fmt.Printf("Now: %v\n", now)
// 格式化时间:Go 使用参考时间 "2006-01-02 15:04:05" 作为格式化模板
// 注意:Go 不使用 YYYY-MM-DD 这种格式串,而是用具体日期作为模板
fmt.Printf("RFC3339: %v\n", now.Format(time.RFC3339))
fmt.Printf("Custom: %v\n", now.Format("2006-01-02 15:04:05"))
fmt.Printf("Date only: %v\n", now.Format("2006-01-02"))
fmt.Printf("Time only: %v\n", now.Format("15:04:05"))
// 获取时间各部分
fmt.Printf("Year: %d\n", now.Year())
fmt.Printf("Month: %d (%s)\n", now.Month(), now.Month())
fmt.Printf("Day: %d\n", now.Day())
fmt.Printf("Hour: %d\n", now.Hour())
fmt.Printf("Minute: %d\n", now.Minute())
fmt.Printf("Second: %d\n", now.Second())
fmt.Printf("Nanosecond: %d\n", now.Nanosecond())
fmt.Printf("Weekday: %s\n", now.Weekday())
// Unix 时间戳
fmt.Printf("Unix: %d\n", now.Unix()) // 秒
fmt.Printf("UnixMilli: %d\n", now.UnixMilli()) // 毫秒(Go 1.17+)
fmt.Printf("UnixMicro: %d\n", now.UnixMicro()) // 微秒(Go 1.17+)
fmt.Printf("UnixNano: %d\n", now.UnixNano()) // 纳秒
}
4.2 解析与构造时间
// Package main 演示时间解析与构造
package main
import (
"fmt"
"time"
)
func main() {
// 解析时间:使用与 Format 相同的参考时间模板
t, err := time.Parse("2006-01-02 15:04:05", "2026-07-21 14:30:00")
if err != nil {
fmt.Println("Parse error:", err)
return
}
fmt.Printf("Parsed: %v\n", t)
// 解析 RFC3339 格式(含时区)
t2, err := time.Parse(time.RFC3339, "2026-07-21T14:30:00+08:00")
if err != nil {
fmt.Println("Parse error:", err)
return
}
fmt.Printf("Parsed RFC3339: %v\n", t2)
// 解析时使用指定时区(避免歧义)
loc, _ := time.LoadLocation("Asia/Shanghai")
t3, err := time.ParseInLocation("2006-01-02 15:04:05", "2026-07-21 14:30:00", loc)
if err != nil {
fmt.Println("Parse error:", err)
return
}
fmt.Printf("Parsed in Shanghai: %v\n", t3)
// 构造指定时间
birthday := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
fmt.Printf("Birthday: %v\n", birthday)
// 通过 Unix 时间戳构造
fromUnix := time.Unix(1700000000, 0)
fmt.Printf("From Unix: %v\n", fromUnix)
}
4.3 时区处理
// Package main 演示时区转换
package main
import (
"fmt"
"time"
)
func main() {
// 当前 UTC 时间
utc := time.Now().UTC()
fmt.Printf("UTC: %v\n", utc)
// 当前本地时间
local := time.Now().Local()
fmt.Printf("Local: %v\n", local)
// 加载 IANA 时区
locs := []string{"Asia/Shanghai", "America/New_York", "Europe/London", "Asia/Tokyo"}
for _, name := range locs {
loc, err := time.LoadLocation(name)
if err != nil {
fmt.Printf("LoadLocation %s: %v\n", name, err)
continue
}
fmt.Printf("%s: %v\n", name, utc.In(loc))
}
// 固定偏移时区(不需要 IANA 数据库)
offset := time.FixedZone("UTC+8", 8*3600)
fmt.Printf("UTC+8 fixed: %v\n", utc.In(offset))
// 时区转换:将本地时间转为 UTC,再转为东京时间
tokyo, _ := time.LoadLocation("Asia/Tokyo")
original := time.Now()
utc2 := original.UTC()
tokyoTime := utc2.In(tokyo)
fmt.Printf("Local %v -> UTC %v -> Tokyo %v\n", original, utc2, tokyoTime)
}
4.4 Duration 与时间运算
// Package main 演示 Duration 与时间加减运算
package main
import (
"fmt"
"time"
)
func main() {
// Duration 是 int64 纳秒值
var d time.Duration = 2*time.Hour + 30*time.Minute + 45*time.Second
fmt.Printf("Duration: %v\n", d)
fmt.Printf("Hours: %v\n", d.Hours())
fmt.Printf("Minutes: %v\n", d.Minutes())
fmt.Printf("Seconds: %v\n", d.Seconds())
fmt.Printf("Milliseconds: %v\n", d.Milliseconds())
fmt.Printf("Microseconds: %v\n", d.Microseconds())
fmt.Printf("Nanoseconds: %v\n", d.Nanoseconds())
// 时间加法
now := time.Now()
future := now.Add(2 * time.Hour)
past := now.Add(-24 * time.Hour)
fmt.Printf("Now: %v\n", now)
fmt.Printf("+2h: %v\n", future)
fmt.Printf("-24h: %v\n", past)
// 时间差
diff := future.Sub(now)
fmt.Printf("Diff: %v\n", diff)
// 自指定时间起经过的时长(优先使用单调时钟)
since := time.Since(now)
fmt.Printf("Since: %v\n", since)
// 到指定时间还剩多久
until := time.Until(future)
fmt.Printf("Until: %v\n", until)
// 截断到指定精度
truncated := now.Truncate(time.Hour)
fmt.Printf("Truncated to hour: %v\n", truncated)
// 四舍五入到指定精度
rounded := now.Round(time.Minute)
fmt.Printf("Rounded to minute: %v\n", rounded)
}
4.5 Timer 与 Ticker
// Package main 演示 Timer 与 Ticker 的使用
package main
import (
"fmt"
"time"
)
func main() {
// Timer:一次性定时器
timer := time.NewTimer(2 * time.Second)
fmt.Println("Waiting for timer...")
<-timer.C // 阻塞直到 timer 触发
fmt.Println("Timer fired")
// time.After 是 Timer 的语法糖
<-time.After(1 * time.Second)
fmt.Println("After fired")
// Ticker:周期性触发
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() // 重要:务必停止 ticker,避免泄漏
done := make(chan bool)
go func() {
// 计数 3 次后停止
count := 0
for {
select {
case t := <-ticker.C:
count++
fmt.Printf("Tick %d: %v\n", count, t.Format("15:04:05.000"))
if count >= 3 {
done <- true
return
}
case <-done:
return
}
}
}()
<-done
fmt.Println("Ticker stopped")
}
4.6 超时控制模式
// Package main 演示基于 time 的超时控制模式
package main
import (
"context"
"fmt"
"time"
)
// SlowOperation 模拟一个可能耗时的操作
func SlowOperation() string {
time.Sleep(3 * time.Second)
return "result"
}
// WithTimeout 使用 select + time.After 实现超时
func WithTimeout() {
result := make(chan string)
go func() {
result <- SlowOperation()
}()
select {
case r := <-result:
fmt.Println("Got result:", r)
case <-time.After(1 * time.Second):
fmt.Println("Timeout!")
}
}
// WithContext 使用 context.WithTimeout 实现超时(推荐)
func WithContext() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
result := make(chan string)
go func() {
result <- SlowOperation()
}()
select {
case r := <-result:
fmt.Println("Got result:", r)
case <-ctx.Done():
fmt.Println("Context timeout:", ctx.Err())
}
}
func main() {
WithTimeout()
WithContext()
}
4.7 Timer.Stop 与 Reset 的正确用法
// Package main 演示 Timer.Stop 与 Reset 的正确用法
// 这是 Go 并发编程中最容易出错的地方之一
package main
import (
"fmt"
"time"
)
// WrongTimerStop 错误示例:未处理 Stop 返回值
func WrongTimerStop() {
t := time.NewTimer(1 * time.Second)
go func() {
// 假设某些条件下需要提前停止
t.Stop() // 若 timer 已触发,C 中已有值,但 Stop 返回 false
// 此时下一次 Reset 不会清空 C,导致下一次接收立即返回旧值
}()
<-t.C
fmt.Println("Timer fired")
}
// CorrectTimerStop 正确示例:检查 Stop 返回值并清空 channel
func CorrectTimerStop() {
t := time.NewTimer(1 * time.Second)
if !t.Stop() {
// Stop 返回 false 表示 timer 已触发或已停止
// 需要清空 channel,避免下一次 Reset 立即返回
select {
case <-t.C:
default:
}
}
// 现在可以安全 Reset
t.Reset(2 * time.Second)
<-t.C
fmt.Println("Reset timer fired")
}
func main() {
CorrectTimerStop()
}
4.8 心跳检测模式
// Package main 演示基于 Ticker 的心跳检测
package main
import (
"fmt"
"sync"
"time"
)
// Heartbeat 心跳发送器
type Heartbeat struct {
interval time.Duration
stop chan struct{}
beat chan time.Time
}
// NewHeartbeat 创建心跳发送器
func NewHeartbeat(interval time.Duration) *Heartbeat {
return &Heartbeat{
interval: interval,
stop: make(chan struct{}),
beat: make(chan time.Time, 1),
}
}
// Start 启动心跳
func (h *Heartbeat) Start() {
ticker := time.NewTicker(h.interval)
go func() {
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
select {
case h.beat <- t:
default:
// 无消费者,跳过本次心跳
}
case <-h.stop:
return
}
}
}()
}
// Stop 停止心跳
func (h *Heartbeat) Stop() {
close(h.stop)
}
// Beat 返回心跳通道
func (h *Heartbeat) Beat() <-chan time.Time {
return h.beat
}
func main() {
hb := NewHeartbeat(500 * time.Millisecond)
hb.Start()
defer hb.Stop()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
count := 0
for t := range hb.Beat() {
count++
fmt.Printf("Heartbeat %d: %v\n", count, t.Format("15:04:05.000"))
if count >= 5 {
return
}
}
}()
wg.Wait()
}
4.9 时间格式化参考
// Package main 演示 Go 时间格式化的特殊语法
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2026, 7, 21, 14, 30, 45, 123456789, time.UTC)
// Go 使用参考时间 "Mon Jan 2 15:04:05 MST 2006" 作为格式化模板
// 该时间为 2006-01-02 15:04:05 -0700,记忆口诀"1月2日下午3点4分5秒 06年"
// 预定义格式
fmt.Println("RFC3339:", t.Format(time.RFC3339))
fmt.Println("RFC3339Nano:", t.Format(time.RFC3339Nano))
fmt.Println("Kitchen:", t.Format(time.Kitchen))
fmt.Println("Stamp:", t.Format(time.Stamp))
fmt.Println("StampMilli:", t.Format(time.StampMilli))
// 自定义格式
fmt.Println("Full date:", t.Format("2006-01-02"))
fmt.Println("Full time:", t.Format("15:04:05"))
fmt.Println("DateTime:", t.Format("2006-01-02 15:04:05"))
fmt.Println("With TZ:", t.Format("2006-01-02 15:04:05 -07:00"))
fmt.Println("With TZ name:", t.Format("2006-01-02 15:04:05 MST"))
fmt.Println("Nano:", t.Format("2006-01-02 15:04:05.999999999"))
fmt.Println("Milli:", t.Format("2006-01-02 15:04:05.000"))
// 解析后格式化(往返测试)
original := "2026-07-21T14:30:45.123456789Z"
parsed, _ := time.Parse(time.RFC3339Nano, original)
roundtrip := parsed.Format(time.RFC3339Nano)
fmt.Println("Roundtrip OK:", original == roundtrip)
}
4.10 自定义时间解析器
// Package main 演示如何解析多种常见的时间格式
package main
import (
"fmt"
"time"
)
// ParseFlexibly 尝试多种格式解析时间字符串
func ParseFlexibly(s string) (time.Time, error) {
formats := []string{
time.RFC3339Nano,
time.RFC3339,
"2006-01-02 15:04:05",
"2006-01-02 15:04:05.999999",
"2006-01-02",
"2006/01/02 15:04:05",
"2006/01/02",
"01/02/2006",
"02-Jan-2006",
"Jan 2, 2006",
"January 2, 2006",
}
var lastErr error
for _, layout := range formats {
t, err := time.Parse(layout, s)
if err == nil {
return t, nil
}
lastErr = err
}
return time.Time{}, fmt.Errorf("cannot parse %q: %w", s, lastErr)
}
func main() {
inputs := []string{
"2026-07-21T14:30:45Z",
"2026-07-21 14:30:45",
"2026-07-21",
"2026/07/21 14:30:45",
"Jul 21, 2026",
}
for _, input := range inputs {
t, err := ParseFlexibly(input)
if err != nil {
fmt.Printf("Failed to parse %q: %v\n", input, err)
continue
}
fmt.Printf("Parsed %q -> %v\n", input, t.Format(time.RFC3339))
}
}
5. 对比分析
5.1 Go time 与其他语言时间库对比
| 语言 | 库 | 单调时钟 | 时区 | Duration | 易用性 |
|---|---|---|---|---|---|
| Go | time | 内置 | IANA | time.Duration | 中 |
| Java | java.time (JSR 310) | Instant | IANA | java.time.Duration | 高 |
| Python | datetime | 无内置 | pytz | timedelta | 中 |
| Rust | chrono | Instant | chrono-tz | Duration | 高 |
| C++ | <chrono> | steady_clock | ICU | duration | 低 |
5.2 time.Sleep vs runtime.Gosched vs time.After
| 方式 | 用途 | 行为 | 精度 |
|---|---|---|---|
time.Sleep(d) | 阻塞当前 goroutine | 让出 P,timer 到期后唤醒 | 毫秒级 |
runtime.Gosched() | 主动让出 | 仅让出一次,立即重新排队 | 不阻塞 |
<-time.After(d) | 等待 channel | 创建 Timer,C 接收后唤醒 | 毫秒级 |
select{case <-c: case <-time.After(d):} | 超时控制 | 创建一次性 Timer | 毫秒级 |
5.3 time.Now 性能对比
| 语言 | 调用 | 开销(ns) |
|---|---|---|
Go time.Now() | vDSO | ~30 |
C clock_gettime | syscall | ~25 |
Java Instant.now() | JNI | ~80 |
Python datetime.now() | C 扩展 | ~200 |
5.4 Timer vs Ticker vs AfterFunc
| 方式 | 触发次数 | 内存模型 | 适用场景 |
|---|---|---|---|
time.NewTimer(d) | 1 次 | 创建 + 销毁开销 | 单次延迟 |
time.NewTicker(d) | 周期 | 创建后常驻 | 心跳、轮询 |
time.After(d) | 1 次 | 简化语法糖 | select 超时 |
time.AfterFunc(d, f) | 1 次 | 在新 goroutine 调用 | 延迟回调 |
time.Tick(d) | 周期 | 无法停止 | 仅短期测试 |
6. 常见陷阱与反模式
6.1 反模式:循环中使用 time.After 导致内存泄漏
// 反模式:每次循环创建新 Timer,未消费的 Timer 会驻留堆中直到触发
func WatchWithLeak(events <-chan int) {
for {
select {
case ev := <-events:
fmt.Println("Event:", ev)
case <-time.After(5 * time.Minute):
// 每次 select 都会创建一个 5 分钟的 Timer
// 在事件频繁时,Timer 在堆中堆积,导致内存泄漏
fmt.Println("Timeout")
}
}
}
// 正确写法:使用 NewTimer + Reset,或使用 context
func WatchWithoutLeak(events <-chan int) {
timer := time.NewTimer(5 * time.Minute)
defer timer.Stop()
for {
timer.Reset(5 * time.Minute)
select {
case ev := <-events:
fmt.Println("Event:", ev)
case <-timer.C:
fmt.Println("Timeout")
return
}
}
}
6.2 反模式:未处理 Timer.Stop 返回值
// 反模式:未检查 Stop 返回值
t := time.NewTimer(1 * time.Second)
go func() {
if condition {
t.Stop() // 若返回 false,C 中可能已有值
}
}()
select {
case <-t.C:
// 即使 Stop 了也可能进入这里
case <-other:
}
// 正确写法
if !t.Stop() {
select {
case <-t.C:
default:
}
}
t.Reset(2 * time.Second)
6.3 反模式:使用 time.Tick 而非 NewTicker
// 反模式:time.Tick 创建的 Ticker 无法停止,导致内存泄漏
for range time.Tick(time.Second) {
// 该 Ticker 永远不会被 GC
}
// 正确写法
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
// ...
}
6.4 反模式:忽略时区
// 反模式:未指定时区,导致跨地区服务时间不一致
t, _ := time.Parse("2006-01-02 15:04:05", "2026-07-21 14:30:00")
// t 默认 UTC,但开发者可能以为是本地时间
// 正确写法:明确指定时区
loc, _ := time.LoadLocation("Asia/Shanghai")
t, _ := time.ParseInLocation("2006-01-02 15:04:05", "2026-07-21 14:30:00", loc)
6.5 反模式:Duration 与整数混淆
// 反模式:未使用 time.Duration 单位
time.Sleep(5) // 5 纳秒,几乎无效果
// 正确写法
time.Sleep(5 * time.Second)
time.Sleep(5 * time.Minute)
6.6 反模式:在循环中重新分配 Time 字符串
// 反模式:循环内反复 Format
for _, t := range timestamps {
log.Printf("time=%s", t.Format(time.RFC3339)) // 每次 Format 分配新字符串
}
// 优化:使用 strconv 或 sync.Pool
// (Format 本身已经较快,但极端高频场景可缓存)
6.7 生产事故案例:闰秒导致服务挂死
2015 年 6 月 30 日 23:59:60 的闰秒,导致大量 Java、Python、C++ 应用陷入 CPU 100% 死循环。Go 应用因使用 monotonic clock 而未受影响,但仍需注意:
- Go 1.9 之前的版本会受影响。
- 跨语言调用(如通过 CGO 调用 C 库)可能仍受影响。
缓解方案:使用 CLOCK_TAI(国际原子时,无闰秒),或在闰秒前后手动暂停服务。
6.8 生产事故案例:虚拟机迁移时钟跳变
某云服务中,Go 服务在虚拟机热迁移后,time.Since(start) 返回负值,导致计费逻辑异常。
根因:虚拟机迁移后 wall clock 跳变,但 Go 1.9+ 使用 monotonic clock 应该不受影响。
排查:服务使用的是 Go 1.8 版本,未启用单调时钟。
修复:升级到 Go 1.9+,并保证所有计时使用 time.Since 而非 time.Now().Sub(start)。
6.9 生产事故案例:时区数据库缺失
某容器化部署的 Go 服务调用 time.LoadLocation("Asia/Shanghai") 报错,因为基础镜像(基于 Alpine)未安装 tzdata。
根因:Alpine 默认不包含 tzdata。
修复方案:
- Dockerfile 添加
apk add --no-cache tzdata。 - 或在 Go 程序中
import _ "time/tzdata"(推荐,便于生成自包含二进制)。
7. 工程实践
7.1 时间存储与序列化最佳实践
- 数据库存储 UTC 时间:避免时区歧义,便于跨地区查询。
- API 传输使用 RFC3339:含时区信息,标准化格式。
- 日志记录 UTC 时间:便于日志聚合(如 ELK)按时间排序。
- 用户界面显示本地时间:在前端根据用户时区转换。
// 数据库存储
db.Exec("INSERT INTO events (ts) VALUES (?)", time.Now().UTC())
// API 响应
type Response struct {
Timestamp string `json:"timestamp"`
}
resp := Response{Timestamp: time.Now().UTC().Format(time.RFC3339)}
// 日志
log.Printf("[%s] event", time.Now().UTC().Format(time.RFC3339))
7.2 性能优化清单
- 避免在热路径 Format 时间:缓存格式化结果。
- 使用 UnixNano 而非 Format:日志时间用 UnixNano,查询时再格式化。
- 复用 Timer:避免反复创建销毁,使用 Reset。
- 批量时间比较:使用 Unix 时间戳而非 Time 对象比较。
- 使用 monotonic clock:Go 1.9+ 默认使用,但避免
time.Date构造的时间。
7.3 测试时间相关代码
测试时间相关代码的常见技巧:
// 使用依赖注入,便于替换 Now 函数
type Clock interface {
Now() time.Time
}
type realClock struct{}
func (realClock) Now() time.Time { return time.Now() }
type fakeClock struct{ t time.Time }
func (f fakeClock) Now() time.Time { return f.t }
// 业务代码使用 Clock 接口
type Service struct{ clock Clock }
func (s *Service) IsExpired(t time.Time, ttl time.Duration) bool {
return s.clock.Now().Sub(t) > ttl
}
// 测试
func TestIsExpired(t *testing.T) {
now := time.Date(2026, 7, 21, 0, 0, 0, 0, time.UTC)
svc := &Service{clock: fakeClock{t: now}}
if !svc.IsExpired(now.Add(-2*time.Hour), time.Hour) {
t.Error("expected expired")
}
}
7.4 时间窗口与滑动窗口
// Package main 演示滑动窗口限流的基础实现
package main
import (
"sync"
"time"
)
// SlidingWindow 滑动窗口计数器
type SlidingWindow struct {
mu sync.Mutex
window time.Duration
limit int64
requests []time.Time
}
func NewSlidingWindow(window time.Duration, limit int64) *SlidingWindow {
return &SlidingWindow{
window: window,
limit: limit,
}
}
// Allow 检查并记录请求
func (s *SlidingWindow) Allow() bool {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
// 清理过期请求
cutoff := now.Add(-s.window)
idx := 0
for i, t := range s.requests {
if t.After(cutoff) {
idx = i
break
}
}
s.requests = s.requests[idx:]
if int64(len(s.requests)) >= s.limit {
return false
}
s.requests = append(s.requests, now)
return true
}
7.5 定时任务调度
// Package main 演示一个简单的定时任务调度器
package main
import (
"fmt"
"sort"
"sync"
"time"
)
// Task 定时任务
type Task struct {
Name string
Next time.Time
Interval time.Duration
Callback func()
}
// Scheduler 任务调度器
type Scheduler struct {
mu sync.Mutex
tasks []*Task
stop chan struct{}
}
func NewScheduler() *Scheduler {
return &Scheduler{stop: make(chan struct{})}
}
func (s *Scheduler) Add(name string, interval time.Duration, cb func()) {
s.mu.Lock()
defer s.mu.Unlock()
s.tasks = append(s.tasks, &Task{
Name: name,
Next: time.Now().Add(interval),
Interval: interval,
Callback: cb,
})
// 按 Next 排序
sort.Slice(s.tasks, func(i, j int) bool {
return s.tasks[i].Next.Before(s.tasks[j].Next)
})
}
func (s *Scheduler) Start() {
go func() {
for {
s.mu.Lock()
if len(s.tasks) == 0 {
s.mu.Unlock()
select {
case <-s.stop:
return
case <-time.After(time.Second):
continue
}
}
next := s.tasks[0].Next
now := time.Now()
s.mu.Unlock()
if now.Before(next) {
select {
case <-time.After(next.Sub(now)):
case <-s.stop:
return
}
}
s.mu.Lock()
if len(s.tasks) > 0 && !time.Now().Before(s.tasks[0].Next) {
task := s.tasks[0]
task.Callback()
task.Next = time.Now().Add(task.Interval)
sort.Slice(s.tasks, func(i, j int) bool {
return s.tasks[i].Next.Before(s.tasks[j].Next)
})
}
s.mu.Unlock()
}
}()
}
func (s *Scheduler) Stop() {
close(s.stop)
}
func main() {
sched := NewScheduler()
sched.Add("task1", 1*time.Second, func() {
fmt.Println("Task1:", time.Now().Format("15:04:05"))
})
sched.Add("task2", 2*time.Second, func() {
fmt.Println("Task2:", time.Now().Format("15:04:05"))
})
sched.Start()
defer sched.Stop()
time.Sleep(10 * time.Second)
}
8. 案例研究
8.1 案例一:分布式系统中的时钟同步
某金融交易系统要求多节点之间事件顺序一致。原始方案使用 NTP 同步,但精度仅毫秒级,导致并发交易顺序混乱。
解决方案:
- 采用 HLC(Hybrid Logical Clock):结合物理时钟与逻辑时钟。
- 物理时钟使用 PTP:通过 IEEE 1588 将精度提升到亚微秒级。
- Go 实现参考:
github.com/hashicorp/serf中的 HLC 实现。
// HLC 简化实现
type HLC struct {
mu sync.Mutex
wallTime int64 // 物理时钟(纳秒)
logical int64 // 逻辑时钟
}
func (h *HLC) Now() (int64, int64) {
h.mu.Lock()
defer h.mu.Unlock()
now := time.Now().UnixNano()
if now > h.wallTime {
h.wallTime = now
h.logical = 0
} else {
h.logical++
}
return h.wallTime, h.logical
}
func (h *HLC) Update(remoteWall, remoteLogical int64) {
h.mu.Lock()
defer h.mu.Unlock()
now := time.Now().UnixNano()
if now > remoteWall && now > h.wallTime {
h.wallTime = now
h.logical = 0
} else if remoteWall > h.wallTime {
h.wallTime = remoteWall
h.logical = remoteLogical + 1
} else if h.wallTime > remoteWall {
h.logical++
} else {
if remoteLogical > h.logical {
h.logical = remoteLogical + 1
} else {
h.logical++
}
}
}
8.2 案例二:Kubernetes 中的 Lease 机制
Kubernetes 使用 Lease 对象实现 leader election。每个节点定期(默认 10s)更新 Lease 的 renewTime,若超过 leaseDurationSeconds 未更新,则认为节点失联。
Go 实现关键代码(简化):
type LeaderElector struct {
identity string
leaseDur time.Duration
renewDeadline time.Duration
retryPeriod time.Duration
}
func (l *LeaderElector) Run(ctx context.Context) {
// 周期性尝试获取或续约 lease
ticker := time.NewTicker(l.retryPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if l.tryAcquireOrRenew() {
// 续约成功
}
case <-ctx.Done():
return
}
}
}
8.3 案例三:Prometheus 中的时间序列
Prometheus 使用毫秒精度的时间戳存储时间序列。Go 客户端通过 time.Now().UnixMilli() 获取时间戳:
type Sample struct {
Timestamp int64 // Unix 毫秒
Value float64
}
func collect() Sample {
return Sample{
Timestamp: time.Now().UnixMilli(),
Value: readMetric(),
}
}
Prometheus 选择毫秒而非纳秒,原因:
- 网络传输更紧凑(int64 毫秒约 19 位数字,纳秒约 19 位但占用更大)。
- 时间精度对监控足够(采样间隔通常 ≥ 15s)。
- 便于与人类阅读的时间戳对齐。
8.4 案例四:gRPC 中的超时传播
gRPC 通过 metadata 在调用链中传播超时:
// 客户端设置 5 秒超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.Call(ctx, req)
// 服务端从 ctx 读取剩余时间
remaining, ok := ctx.Deadline()
if ok {
fmt.Println("Time remaining:", time.Until(remaining))
}
gRPC 将 grpc-timeout header 编码为字符串传输,支持多种精度:
1H:1 小时5M:5 分钟30S:30 秒500m:500 毫秒100u:100 微秒50n:50 纳秒
9.1 基础题
题 1:解释 Go time.Time 内部存储的字段及其作用。
题 2:以下代码输出什么?为什么?
t1 := time.Now()
time.Sleep(1 * time.Second)
t2 := time.Now()
fmt.Println(t2.Sub(t1))
fmt.Println(t2.Before(t1))
题 3:将字符串 “2026-07-21 14:30:00” 解析为 Asia/Shanghai 时区的 time.Time。
9.2 进阶题
题 4:实现一个 CronTimer,支持解析 cron 表达式(简化版,仅支持 * * * * * 五段格式),并在指定时刻触发回调。
题 5:分析以下代码的内存泄漏问题,并给出修复方案:
func Watch(events <-chan int) {
for {
select {
case <-events:
case <-time.After(time.Hour):
return
}
}
}
题 6:实现一个 RateLimiter,基于滑动窗口算法,每秒允许 N 个请求。
9.3 挑战题
题 7:实现一个跨时区的时间格式化服务,要求:
- 输入 UTC 时间戳。
- 输出指定时区的本地时间字符串。
- 支持夏令时自动调整。
- 支持 RFC3339、ISO8601、Unix 时间戳等多种输出格式。
题 8:设计一个分布式定时任务调度系统,要求:
- 支持单点故障转移。
- 任务精确触发(误差 < 100ms)。
- 支持任务依赖与重试。
- 考虑时钟漂移与网络延迟。
10.1 经典论文与文献
[1] Lamport, L. 1978. Time, clocks, and the ordering of events in a distributed system. Communications of the ACM 21, 7 (July 1978), 558–565. DOI: https://doi.org/10.1145/359545.359563
[2] Mills, D. L. 1991. Internet time synchronization: the network time protocol. IEEE Transactions on Communications 39, 10 (Oct. 1991), 1482–1493. DOI: https://doi.org/10.1109/26.103043
[3] Kuhn, M. 2003. Leap seconds and the network time protocol. Available: https://www.cl.cam.ac.uk/~mgk25/time/leap/
[4] Corbett, J. C. et al. 2012. Spanner: Google’s globally-distributed database. ACM Transactions on Computer Systems 31, 3 (Aug. 2012), Article 8. DOI: https://doi.org/10.1145/2491245
[5] Kulkarni, S. S., Demirbas, M., Madappa, D., and Avula, B. 2014. Logical physical clocks. In Principles of Distributed Systems. OPODIS 2014. DOI: https://doi.org/10.4230/LIPIcs.OPODIS.2014.17
10.2 标准与规范
[6] IEEE Standard for a Precision Clock Synchronization Protocol for Networked Measurement and Control Systems. 2008. IEEE Std 1588-2008. DOI: https://doi.org/10.1109/IEEESTD.2008.4579760
[7] International Telecommunication Union. 2002. Recommendation ITU-R TF.460-6: Standard-frequency and time-signal emissions. Available: https://www.itu.int/rec/R-REC-TF.460-6-200202-I
10.3 Go 官方文档
[8] The Go Authors. 2024. package time. Go Standard Library Documentation. Available: https://pkg.go.dev/time
[9] The Go Authors. 2017. Go 1.9 Release Notes: Monotonic Time. Available: https://go.dev/doc/go1.9
[10] The Go Authors. 2024. time/tzdata package. Available: https://pkg.go.dev/time/tzdata
10.4 经典工程案例
[11] Popescu, A. et al. 2015. The Leap Second’s Effect on Linux and Java. Cloudflare Blog. Available: https://blog.cloudflare.com/how-and-why-the-leap-second-affected-cloudflare-dns/
[12] Burns, B. and Grant, B. 2018. Kubernetes Up & Running. O’Reilly Media. ISBN: 978-1492046530.
[13] Kleppmann, M. 2017. Designing Data-Intensive Applications. O’Reilly Media, Chapter 8: The Trouble with Distributed Systems. ISBN: 978-1491950357.
11.2 经典教材
- Designing Data-Intensive Applications(Martin Kleppmann)第 8 章
- Distributed Systems(Maarten van Steen, Andrew S. Tanenbaum)第 6 章
- Site Reliability Engineering(Google)第 6 章:Distributed Cron
11.3 前沿论文
- TrueTime: Clock Synchronization in Spanner(Google, 2012)
- HLC: Hybrid Logical Clocks for Distributed Systems(OPODIS 2014)
- CRDTs and Time in Distributed Systems(PLOS 2022)
11.4 相关开源项目
github.com/robfig/cron:Go 生态最流行的 cron 库github.com/go-co-op/gocron:现代化 cron 调度器github.com/hashicorp/serf:包含 HLC 实现github.com/beevik/ntp:NTP 客户端github.com/jinzhu/now:时间工具库
11.5 进阶主题
- Cron 表达式解析:标准 cron 与 Quartz cron 的差异。
- 时区数据库更新:IANA 每年发布 4-8 次更新,如何热更新?
- PTP 协议:IEEE 1588 在数据中心的应用。
- Lamport Clock 与 Vector Clock:分布式逻辑时钟。
- Event Sourcing:基于时间的事件溯源模式。
12. 附录
12.1 Go 时间格式参考表
| 含义 | 模板 | 示例 |
|---|---|---|
| 年 | 2006 | 2026 |
| 月(数字) | 01 | 07 |
| 月(缩写) | Jan | Jul |
| 月(全名) | January | July |
| 日 | 02 | 21 |
| 日(无前导零) | 2 | 21 |
| 星期(缩写) | Mon | Mon |
| 星期(全名) | Monday | Monday |
| 时(24小时) | 15 | 14 |
| 时(12小时) | 3 | 2 |
| 分 | 04 | 30 |
| 秒 | 05 | 45 |
| 毫秒 | .000 | .123 |
| 微秒 | .000000 | .123456 |
| 纳秒 | .000000000 | .123456789 |
| 时区名 | MST | UTC |
| 时区偏移 | -07:00 | +08:00 |
12.2 常用时间常量
| 常量 | 值 | 说明 |
|---|---|---|
time.Nanosecond | 1 ns | 纳秒 |
time.Microsecond | 1000 ns | 微秒 |
time.Millisecond | 1000000 ns | 毫秒 |
time.Second | 10^9 ns | 秒 |
time.Minute | 6×10^10 ns | 分钟 |
time.Hour | 36×10^11 ns | 小时 |
time.RFC3339 | “2006-01-02T15:04:05Z07:00” | RFC3339 |
time.RFC3339Nano | “2006-01-02T15:04:05.999999999Z07:00” | 含纳秒 |
time.Kitchen | “3:04PM” | 简短时间 |
time.Stamp | “Jan _2 15:04:05” | 时间戳 |
time.StampMilli | “Jan _2 15:04:05.000” | 含毫秒 |
12.3 IANA 时区列表(部分)
| 时区 | 偏移 | 夏令时 |
|---|---|---|
| UTC | +00:00 | 无 |
| Asia/Shanghai | +08:00 | 无 |
| Asia/Tokyo | +09:00 | 无 |
| Asia/Kolkata | +05:30 | 无 |
| America/New_York | -05:00 / -04:00 | 有 |
| America/Los_Angeles | -08:00 / -07:00 | 有 |
| Europe/London | +00:00 / +01:00 | 有 |
| Europe/Paris | +01:00 / +02:00 | 有 |
| Australia/Sydney | +10:00 / +11:00 | 有 |
12.4 性能基准测试模板
func BenchmarkTimeNow(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = time.Now()
}
}
func BenchmarkTimeFormat(b *testing.B) {
t := time.Now()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = t.Format(time.RFC3339)
}
}
func BenchmarkTimeParse(b *testing.B) {
s := "2026-07-21T14:30:45Z"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = time.Parse(time.RFC3339, s)
}
}
func BenchmarkTimerCreate(b *testing.B) {
for i := 0; i < b.N; i++ {
t := time.NewTimer(time.Hour)
t.Stop()
}
}
func BenchmarkTimeAdd(b *testing.B) {
t := time.Now()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = t.Add(time.Hour)
}
}
12.5 time 包 API 速查表
| 操作 | 函数/方法 | 说明 |
|---|---|---|
| 当前时间 | time.Now() | 含单调时钟 |
| 当前 UTC | time.Now().UTC() | UTC 时区 |
| 构造时间 | time.Date(y, m, d, h, m, s, ns, loc) | 构造指定时间 |
| Unix 时间戳 | time.Unix(sec, nsec) | 从时间戳构造 |
| 解析时间 | time.Parse(layout, value) | 按布局解析(UTC) |
| 解析带时区 | time.ParseInLocation(layout, value, loc) | 按布局与时区解析 |
| 格式化 | t.Format(layout) | 按布局格式化 |
| 加法 | t.Add(d) | 加 Duration |
| 减法 | t.Sub(t2) | 返回 Duration |
| 自此刻起 | time.Since(t) | 等价 Now().Sub(t) |
| 到此刻止 | time.Until(t) | 等价 t.Sub(Now()) |
| 截断 | t.Truncate(d) | 截断到指定精度 |
| 四舍五入 | t.Round(d) | 四舍五入到指定精度 |
| 时区转换 | t.In(loc) | 转换时区 |
| 是否相等 | t.Equal(t2) | 比较两个时刻(不受时区影响) |
| 是否在…之前 | t.Before(t2) | 时间顺序比较 |
| 是否在…之后 | t.After(t2) | 时间顺序比较 |
| 是否零值 | t.IsZero() | 是否 0001-01-01 |
| 休眠 | time.Sleep(d) | 阻塞当前 goroutine |
| 一次性定时器 | time.NewTimer(d) | 返回 Timer |
| 周期定时器 | time.NewTicker(d) | 返回 Ticker |
| 延迟回调 | time.AfterFunc(d, f) | 在新 goroutine 调用 |
| 加载时区 | time.LoadLocation(name) | 加载 IANA 时区 |
结语
时间是软件工程中最隐蔽的复杂度来源。看似简单的 time.Now() 背后蕴含了 IEEE 1588、NTP、单调时钟、闰秒、时区数据库等大量工程妥协。Go time 包通过同时存储 wall clock 与 monotonic clock,巧妙地解决了 NTP 跳变问题;通过 IANA tzdata 集成,提供了开箱即用的全球时区支持;通过 Timer/Ticker 与 channel 的结合,构建了优雅的并发时间原语。
掌握 Go 时间编程,意味着能够在分布式系统中正确处理时钟漂移、闰秒、时区转换、超时控制、定时任务等关键场景。本文从物理学基础、操作系统时钟、Go runtime 实现、生产级陷阱到工程最佳实践,希望为读者构建一套完整的时间编程知识体系。
记住三条工程红线:
- 永远使用
time.Since而非time.Now().Sub,前者优先使用单调时钟。 - 永远使用
time.NewTicker而非time.Tick,前者可显式停止。 - 永远在数据库与 API 中存储 UTC 时间,仅在前端展示时转换为本地时区。
当前时间
基本写法:获取当前时间
time.Now() time.Time
// 返回当前本地时间
now := time.Now()
基本写法:获取当前 Unix 时间戳
time.Now().Unix() int64
// 返回自 1970-01-01 起的秒数
sec := time.Now().Unix()
基本写法:获取纳秒时间戳
time.Now().UnixNano() int64
// 返回纳秒级时间戳,用于高精度计时
nano := time.Now().UnixNano()
基本写法:获取 Unix 毫秒
time.Now().UnixMilli() int64
// 返回毫秒时间戳(Go 1.17+)
ms := time.Now().UnixMilli()
时间创建
基本写法:构造指定时间
time.Date(<年>, <月>, <日>, <时>, <分>, <秒>, <纳秒>, <时区>) time.Time
// 构造 2024 年 1 月 1 日 0 点 UTC 时间
t := time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)
基本写法:从 Unix 秒创建
time.Unix(<秒>, <纳秒>) time.Time
// 从时间戳还原时间
t := time.Unix(1700000000, 0)
基本写法:解析字符串为时间
time.Parse(<布局>, <字符串>) (time.Time, error)
// 按 RFC3339 布局解析
t, err := time.Parse("2006-01-02 15:04:05", "2024-01-01 12:00:00")
基本写法:按本地时区解析
time.ParseInLocation(<布局>, <字符串>, <时区>) (time.Time, error)
// 按上海时区解析字符串
loc, _ := time.LoadLocation("Asia/Shanghai")
t, _ := time.ParseInLocation("2006-01-02 15:04:05", "2024-01-01 12:00:00", loc)
格式化布局
基本写法:格式化为字符串
<时间>.Format(<布局>) string
// Go 使用参考时间 2006-01-02 15:04:05 作为布局
s := time.Now().Format("2006-01-02 15:04:05")
基本写法:RFC3339 格式
<时间>.Format(time.RFC3339) string
// 输出 2024-01-01T12:00:00Z 格式
s := time.Now().Format(time.RFC3339)
基本写法:自定义布局
<时间>.Format("<布局串>") string
// 常用占位:年 2006 月 01 日 02 时 15 分 04 秒 05
s := time.Now().Format("2006/01/02 15:04")
基本写法:格式化为 RFC3339Nano
<时间>.Format(time.RFC3339Nano) string
// 带纳秒精度的 ISO 格式
s := time.Now().Format(time.RFC3339Nano)
时间运算
基本写法:增加时间
<时间>.Add(<时长>) time.Time
// 当前时间加 2 小时
later := time.Now().Add(2 * time.Hour)
基本写法:增加年月日
<时间>.AddDate(<年>, <月>, <日>) time.Time
// 当前时间加 1 年 2 个月 3 天
t := time.Now().AddDate(1, 2, 3)
基本写法:计算时间差
<时间>.Sub(<另一时间>) time.Duration
// 计算两个时间间隔
diff := end.Sub(start)
基本写法:比较时间先后
<时间>.Before(<另一时间>) bool
// 判断 t1 是否早于 t2
if t1.Before(t2) { }
基本写法:比较时间相等
<时间>.Equal(<另一时间>) bool
// 判断两时间是否相等(推荐用 Equal 而非 ==)
if t1.Equal(t2) { }
基本写法:判断之后
<时间>.After(<另一时间>) bool
// 判断 t1 是否晚于 t2
if t1.After(t2) { }
时长 Duration
基本写法:定义时长
<数值> * time.<单位>
// 常见单位:ns、us、ms、s、m、h
d := 500 * time.Millisecond
基本写法:时长转秒
<时长>.Seconds() float64
// 将时长转为浮点秒
sec := d.Seconds()
基本写法:时长转字符串
<时长>.String() string
// 输出如 500ms、2h30m 的可读形式
s := d.String()
基本写法:解析时长字符串
time.ParseDuration(<字符串>) (time.Duration, error)
// 解析 1h30m 形式时长
d, err := time.ParseDuration("1h30m")
定时器
基本写法:一次性定时器
time.NewTimer(<时长>) *time.Timer
// 2 秒后触发
timer := time.NewTimer(2 * time.Second)
<-timer.C
基本写法:重置定时器
<timer>.Reset(<时长>) bool
// 重新计时(Go 1.23+ 仅在未停止且未触发时返回 true)
timer.Reset(3 * time.Second)
基本写法:停止定时器
<timer>.Stop() bool
// 停止定时器,返回是否成功停止
timer.Stop()
基本写法:周期触发
time.NewTicker(<时长>) *time.Ticker
// 每 1 秒触发一次
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for t := range ticker.C {
fmt.Println(t)
}
基本写法:阻塞等待
time.Sleep(<时长>)
// 暂停当前 goroutine 100 毫秒
time.Sleep(100 * time.Millisecond)
基本写法:After 延迟通道
time.After(<时长>) <-chan time.Time
// 返回到时发送一次的通道
select {
case <-time.After(time.Second):
}
时区
基本写法:获取时区
time.LoadLocation(<名称>) (*time.Location, error)
// 加载上海时区
loc, err := time.LoadLocation("Asia/Shanghai")
基本写法:UTC 时区
time.UTC *time.Location
// 使用 UTC 时区
t := time.Now().In(time.UTC)
基本写法:转换时区
<时间>.In(<时区>) time.Time
// 将时间转换为指定时区表示
t := time.Now().In(loc)
基本写法:固定偏移时区
time.FixedZone(<名称>, <秒偏移>) *time.Location
// 东八区固定偏移
loc := time.FixedZone("CST", 8*3600)
时间戳与组件
基本写法:获取年月日
<时间>.Date() (<年>, <月>, <日>)
// 返回年月日三个值
y, m, d := time.Now().Date()
基本写法:获取时分秒
<时间>.Clock() (<时>, <分>, <秒>)
// 返回时分秒三个值
h, mi, s := time.Now().Clock()
基本写法:获取星期几
<时间>.Weekday() time.Weekday
// 返回星期,0 是 Sunday
w := time.Now().Weekday()
基本写法:获取年内天数
<时间>.YearDay() int
// 返回当年第几天(1-366)
day := time.Now().YearDay()
性能计时
基本写法:高精度计时
time.Since(<起始时间>) time.Duration
// 测量代码执行耗时
start := time.Now()
doWork()
fmt.Println(time.Since(start))
基本写法:单调时钟计时
time.Now() time.Time
// Now 内含单调时钟读数,Sub/Before/After 仅比较单调部分
// 不受系统时间回拨影响