time — 时间处理
time 包提供了时间测量、格式化和定时功能。它是 Go 中最常用的包之一,但也是最容易写出 bug 的包之一(时区、格式字符串、Duration 溢出)。
1. 核心类型
| 类型 | 含义 | 示例 |
|---|---|---|
time.Time |
一个时间点(纳秒精度,带时区) | 2024-01-15 10:30:00 +08:00 |
time.Duration |
时间段(纳秒精度,int64) | 5s, 100ms, 2h30m |
time.Location |
时区 | Asia/Shanghai, UTC |
*time.Timer |
一次性定时器 | time.NewTimer(2s) |
*time.Ticker |
周期性打点器 | time.NewTicker(500ms) |
2. 🔬 深入原理:Go 的时间格式化为什么是 2006-01-02 15:04:05?
Go 使用固定的参考时间来表示格式:
Mon Jan 2 15:04:05 MST 2006这串数字按顺序是:1 2 3 4 5 6 7(月/日/时/分/秒/年/时区)。这个设计意味着你不需要记 %Y %m %d 之类的符号,只要写出参考时间的对应位置即可:
now := time.Now()
now.Format("2006-01-02") // → 2024-01-15
now.Format("15:04:05") // → 10:30:00
now.Format("2006年01月02日") // → 2024年01月15日
now.Format("Mon Jan 2 15:04:05") // → Mon Jan 15 10:30:00
// 常用的预定义格式
now.Format(time.RFC3339) // 2006-01-02T15:04:05Z07:00
now.Format(time.DateTime) // 2006-01-02 15:04:05 (Go 1.20+)🚨 陷阱:
time.RFC3339是带时区的完整表示,适用于 API;但如果你用JSON Marshal/Unmarshal,Go 使用的是RFC3339Nano。
3. Duration — 时间段的正确使用
// 创建 Duration
d := 10 * time.Second
d := time.Duration(500) * time.Millisecond // 类型转换后做乘法
// 常量(在 time 包中定义)
time.Nanosecond // 1
time.Microsecond // 1000
time.Millisecond // 1e6
time.Second // 1e9
time.Minute // 6e10
time.Hour // 3.6e12Duration 的序列化/反序列化
// JSON 中:"5s" → 5 * time.Second
type Config struct {
Timeout Duration `json:"timeout"`
}
// 自定义 Duration 的 JSON 解析
type Duration time.Duration
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).String())
}
func (d *Duration) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
dur, err := time.ParseDuration(s)
if err != nil {
return err
}
*d = Duration(dur)
return nil
}🚨 陷阱:
Duration本质是int64纳秒,最大值约 290 年。直接用int相乘可能溢出但不报错——最终得到负数 Duration。
4. 时区 — 最多坑的部分
4.1 三个关键认知
// 认知 1: time.Now() 返回的时间带本地时区信息
now := time.Now()
fmt.Println(now) // 2024-01-15 10:30:00 +08:00
// 认知 2: time.Parse 默认返回 UTC(除非格式中有时区信息)
t, _ := time.Parse("2006-01-02 15:04:05", "2024-01-15 10:30:00")
fmt.Println(t) // 2024-01-15 10:30:00 +0000 UTC ← 注意!
// 认知 3: 正确做法是使用 ParseInLocation
loc, _ := time.LoadLocation("Asia/Shanghai")
t, _ = time.ParseInLocation("2006-01-02 15:04:05", "2024-01-15 10:30:00", loc)
fmt.Println(t) // 2024-01-15 10:30:00 +0800 CST🚨 顶级陷阱:
time.Parse不指定时区时默认为 UTC,这几乎不是你想要的。始终使用ParseInLocation。
4.2 时区转换
// time.Time.In(loc) 返回"同一时刻"在不同时区的表示
shanghai, _ := time.LoadLocation("Asia/Shanghai")
nyc, _ := time.LoadLocation("America/New_York")
t := time.Date(2024, 1, 15, 12, 0, 0, 0, shanghai)
fmt.Println(t.In(nyc)) // 2024-01-14 23:00:00 -0500 EST
// 注意:同一时刻,纽约还是前一天晚上4.3 时区数据库
Go 使用 IANA 时区数据库。跨平台注意事项:
- Linux/macOS:通常有系统时区数据。
- Windows:Go 1.15+ 内置了时区数据,无需额外配置。
- Docker 精简镜像:可能需要安装
tzdata包,或者使用 Go 1.15+ 的内置数据。
5. 时间计算与判断
5.1 时间加减
now := time.Now()
// 加法(注意返回新值,不修改原值)
tomorrow := now.Add(24 * time.Hour)
// AddDate — 处理月份边界
nextMonth := now.AddDate(0, 1, 0) // 加 1 个月
// 时间差
diff := tomorrow.Sub(now) // 返回 Duration
// 截断/舍入
now.Truncate(time.Hour) // 截断到整点
now.Round(time.Hour) // 四舍五入到整点🚨 陷阱:
AddDate(0, 1, 0)在 1 月 31 日执行得到的不是 2 月 28 日?Go 的AddDate会进行规范化,1月31日 + 1个月→2月31日→ 规范化到3月3日。这和大部分人的预期不同。
5.2 时间比较
t1 := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)
t2 := time.Date(2024, 1, 15, 20, 0, 0, 0, time.FixedZone("CST", 8*60*60))
// Before / After — 比较时刻的先后
t1.Before(t2) // false — 同一时刻!
t1.After(t2) // false — 同一时刻!
// Equal — 比较的是"同一时刻",而非字面值相同
t1.Equal(t2) // true — 虽然显示时间不同,但代表同一时刻
// ⚠️ 注意:字面值相同的两个 time.Time,如果时区不同,Equal 返回 false💡 关键认知:
==和Equal()的区别很大:
t1 == t2:比较结构体的每个字段(包括时区),两个不同时区但相同时刻的时间用==比较返回 false。t1.Equal(t2):比较同一时刻,忽略时区显示差异,返回 true。- 始终用
Equal()比较时间,不要用==。
5.3 零值判断:IsZero
// IsZero — 判断时间是否为 Go 时间的零值
// 零值 = time.Time{}(即 January 1, year 1, 00:00:00 UTC)
var t time.Time // 零值
fmt.Println(t.IsZero()) // true — 还未被赋值
t2 := time.Now()
fmt.Println(t2.IsZero()) // false — 已赋值
// 💡 常见用法:检查可选的时间字段是否被设置
type Event struct {
Title string
StartTime time.Time // 零值表示"未设置"
EndTime time.Time
}
func (e Event) HasSchedule() bool {
return !e.StartTime.IsZero() && !e.EndTime.IsZero()
}
// 🚨 注意:time.Time{} 的 IsZero 为 true,
// 但 time.Unix(-1, 0) 不是零值,IsZero 为 false5.4 其他判断方法
// IsUTC — 时区是否为 UTC
t := time.Now().UTC()
fmt.Println(t.IsUTC()) // true
t2 := time.Now()
fmt.Println(t2.IsUTC()) // false
// IsDST — 是否处于夏令时
loc, _ := time.LoadLocation("America/New_York")
summer := time.Date(2024, 7, 1, 12, 0, 0, 0, loc)
fmt.Println(summer.IsDST()) // true
// YearDay — 一年中的第几天
fmt.Println(time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC).YearDay()) // 32
// Weekday — 获取星期几
weekday := time.Now().Weekday() // time.Monday
fmt.Println(weekday.String()) // "Monday"
// Unix / UnixMilli / UnixMicro / UnixNano — 时间戳
fmt.Println(time.Now().Unix()) // 1705300200 (秒)
fmt.Println(time.Now().UnixMilli()) // 1705300200123 (毫秒)
fmt.Println(time.Now().UnixMicro()) // 1705300200123456 (微秒)
fmt.Println(time.Now().UnixNano()) // 1705300200123456789 (纳秒)6. Timer 和 Ticker
Timer:一次性
// 方式 1:创建 Timer
timer := time.NewTimer(2 * time.Second)
<-timer.C // 等待触发
timer.Stop() // 停止(如果还没触发)
timer.Reset(1*time.Second) // 重置
// 方式 2:直接用 After(但无法 Stop/Reset)
<-time.After(2 * time.Second)
// 方式 3:AfterFunc(触发后执行函数)
timer := time.AfterFunc(2*time.Second, func() {
fmt.Println("fired")
})
timer.Stop() // 可以取消🚨 陷阱:
time.After返回的 channel 直到触发后才释放。在循环中大量创建会导致短期内存泄漏。在 select 循环中应使用Reset复用一个 Timer。
Ticker:周期性的
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
fmt.Println("tick at", t)
// 处理完毕,等待下一个 tick
case <-ctx.Done():
return
}
}7. 经典应用:超时控制
// 模式 1:context + timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := doWork(ctx)
// 模式 2:select + After
select {
case result := <-workCh:
fmt.Println("done:", result)
case <-time.After(3 * time.Second):
fmt.Println("timeout")
}
// 模式 3:Timer + Reset(高效,避免频繁分配)
timer := time.NewTimer(3 * time.Second)
defer timer.Stop()
for {
select {
case result := <-workCh:
if !timer.Stop() {
<-timer.C // 排空
}
timer.Reset(3 * time.Second)
case <-timer.C:
fmt.Println("timeout")
return
}
}8. 🚨 常见陷阱汇总
| 陷阱 | 说明 | 正确做法 |
|---|---|---|
time.Parse 默认 UTC |
解析无时区字符串得到 UTC 时间 | 用 ParseInLocation |
AddDate 月末问题 |
1月31日+1个月 → 3月3日 | 检查结果日期,或使用专门库 |
time.After 在循环中 |
channel 不回收,内存泄漏 | 复用 Timer |
defer cancel() 忘记 |
Context 泄漏 goroutine | lint 工具检查 |
直接比较 == 两个 Time |
时区不同导致相同时刻不相等 | 用 Equal() |
time.Sleep 不能中断 |
sleep 期间 goroutine 不响应取消 | 改用 select + time.After |