Skip to content
Go
time — 时间处理

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.6e12

Duration 的序列化/反序列化

// 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 为 false

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