时间与日期
time.Time 基础
time.Time 是 Go 中表示时间点的核心类型,包含了年、月、日、时、分、秒、纳秒以及时区信息。它是值类型,可以安全地在函数间传递和比较。
获取当前时间
package main
import (
"fmt"
"time"
)
func main() {
// Now — 获取当前本地时间
now := time.Now()
fmt.Println("当前时间:", now)
// 当前时间: 2024-03-15 10:30:00.123456789 +0800 CST
// UTC — 获取 UTC 时间
utc := time.Now().UTC()
fmt.Println("UTC 时间:", utc)
// UTC 时间: 2024-03-15 02:30:00.123456789 +0000 UTC
// Date — 创建指定日期的时间
t := time.Date(2024, 3, 15, 10, 30, 0, 0, time.Local)
fmt.Println("指定时间:", t)
// 指定时间: 2024-03-15 10:30:00 +0800 CST
}提取时间分量
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// 提取日期分量
fmt.Println("年:", now.Year()) // 2024
fmt.Println("月:", now.Month()) // March
fmt.Println("日:", now.Day()) // 15
fmt.Println("小时:", now.Hour()) // 10
fmt.Println("分钟:", now.Minute()) // 30
fmt.Println("秒:", now.Second()) // 0
fmt.Println("纳秒:", now.Nanosecond()) // 123456789
// Weekday — 星期几
fmt.Println("星期:", now.Weekday()) // Friday
// YearDay — 一年中的第几天
fmt.Println("第几天:", now.YearDay()) // 75
// Unix — Unix 时间戳(秒)
fmt.Println("Unix 秒:", now.Unix()) // 1710473400
// UnixMilli — 毫秒时间戳(Go 1.17+)
fmt.Println("Unix 毫秒:", now.UnixMilli())
// UnixMicro — 微秒时间戳(Go 1.17+)
fmt.Println("Unix 微秒:", now.UnixMicro())
// UnixNano — 纳秒时间戳
fmt.Println("Unix 纳秒:", now.UnixNano())
}时间创建与解析
Parse 与 ParseInLocation
package main
import (
"fmt"
"time"
)
func main() {
// Parse — 解析时间字符串(使用默认时区 UTC)
// 注意:Parse 的 layout 参数不是任意格式!
t, err := time.Parse("2006-01-02", "2024-03-15")
if err != nil {
panic(err)
}
fmt.Println(t) // 2024-03-15 00:00:00 +0000 UTC(注意时区为 UTC)
// ParseInLocation — 在指定时区解析时间(推荐!)
loc, _ := time.LoadLocation("Asia/Shanghai")
t2, err := time.ParseInLocation("2006-01-02 15:04:05", "2024-03-15 10:30:00", loc)
if err != nil {
panic(err)
}
fmt.Println(t2) // 2024-03-15 10:30:00 +0800 CST
}Go 的时间格式化参考时间
Go 使用 2006-01-02 15:04:05 作为格式化参考时间(而非 YYYY-MM-DD),这个日期具有特殊含义:
2006→ 年(这是 Go 诞生的年份)01→ 月02→ 日15→ 时(24小时制,15 = 下午3点)04→ 分05→ 秒
必须使用这个日期,其他任何日期都不会被正确识别!
常用 Layout 常量
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// time 包预定义的 Layout 常量
fmt.Println(time.RFC3339) // 2006-01-02T15:04:05Z07:00
fmt.Println(time.RFC3339Nano) // 2006-01-02T15:04:05.999999999Z07:00
fmt.Println(time.RFC1123) // Mon, 02 Jan 2006 15:04:05 MST
fmt.Println(time.RFC850) // Monday, 02-Jan-06 15:04:05 MST
fmt.Println(time.RFC1123Z) // Mon, 02 Jan 2006 15:04:05 -0700
fmt.Println(time.Kitchen) // 3:04PM
fmt.Println(time.Stamp) // Jan _2 15:04:05
fmt.Println(time.DateOnly) // 2006-01-02(Go 1.20+)
fmt.Println(time.TimeOnly) // 15:04:05(Go 1.20+)
// 使用预定义常量格式化
fmt.Println(now.Format(time.RFC3339))
// 2024-03-15T10:30:00+08:00
// 使用预定义常量解析
t, _ := time.Parse(time.RFC3339, "2024-03-15T10:30:00+08:00")
fmt.Println(t)
}时间格式化
Format 方法
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// 自定义格式(必须使用 2006-01-02 15:04:05)
fmt.Println(now.Format("2006-01-02")) // 2024-03-15
fmt.Println(now.Format("2006/01/02 15:04:05")) // 2024/03/15 10:30:00
fmt.Println(now.Format("2006年01月02日 15时04分05秒")) // 2024年03月15日 10时30分00秒
fmt.Println(now.Format("Jan 02, 2006")) // Mar 15, 2024
fmt.Println(now.Format("Monday, January 2, 2006")) // Friday, March 15, 2024
fmt.Println(now.Format("15:04:05.000")) // 10:30:00.123(毫秒)
fmt.Println(now.Format("15:04:05.000000")) // 10:30:00.123456(微秒)
// 常见格式化模式
fmt.Println(now.Format("02 Jan 2006 15:04 MST")) // 15 Mar 2024 10:30 CST
fmt.Println(now.Format("2006-01-02T15:04:05Z07:00")) // 2024-03-15T10:30:00+08:00
// 带星期
fmt.Println(now.Format("2006-01-02 Monday")) // 2024-03-15 Friday
// 时间戳
fmt.Println(now.Unix()) // 1710473400
fmt.Println(now.UnixMilli()) // 1710473400123
}记住这个口诀
Go 的时间格式化参考值 2006-01-02 15:04:05 -0700 可以这样记忆:2006年1月2日 下午3点4分5秒,UTC-7 时区(即美国山地标准时间)。
时间运算
加减、比较
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// Add — 增加时间
fmt.Println(now.Add(1 * time.Hour)) // 1 小时后
fmt.Println(now.Add(30 * time.Minute)) // 30 分钟后
fmt.Println(now.Add(-24 * time.Hour)) // 24 小时前(负数表示减少)
// AddDate — 按日历单位增加
// AddDate(years, months, days)
fmt.Println(now.AddDate(1, 0, 0)) // 1 年后
fmt.Println(now.AddDate(0, 1, 0)) // 1 个月后
fmt.Println(now.AddDate(0, 0, 7)) // 7 天后
fmt.Println(now.AddDate(-1, -2, -3)) // 1年2个月3天前
// Sub — 计算两个时间点的差值
t1 := time.Date(2024, 3, 15, 10, 0, 0, 0, time.Local)
t2 := time.Date(2024, 3, 10, 8, 0, 0, 0, time.Local)
diff := t1.Sub(t2)
fmt.Println("差值:", diff) // 125h0m0s
fmt.Println("差值小时:", diff.Hours()) // 125
fmt.Println("差值天数:", diff/24/time.Hour) // 5
// Before / After / Equal
fmt.Println(t1.After(t2)) // true
fmt.Println(t1.Before(t2)) // false
fmt.Println(t1.Equal(t2)) // false
fmt.Println(t1.Equal(t1)) // true
}Add vs AddDate
Add:按绝对时间增加(例如加 24 小时,夏令时变化时可能跳过/重复某个小时)AddDate:按日历逻辑增加(例如加 1 个月,3月31日 → 4月30日,不会溢出)- 对于”计算到期日”等场景,优先使用
AddDate
Duration 类型
time.Duration 类型表示两个时间点之间的时间段,底层类型是 int64,单位为纳秒。Go 提供了丰富的常量简化 Duration 的创建。
package main
import (
"fmt"
"time"
)
func main() {
// Duration 常量
fmt.Println(time.Nanosecond) // 1ns
fmt.Println(time.Microsecond) // 1µs
fmt.Println(time.Millisecond) // 1ms
fmt.Println(time.Second) // 1s
fmt.Println(time.Minute) // 1m0s
fmt.Println(time.Hour) // 1h0m0s
// 创建 Duration
d := 2*time.Hour + 30*time.Minute + 15*time.Second
fmt.Println(d) // 2h30m15s
fmt.Println(d.Hours()) // 2.504166...
fmt.Println(d.Minutes()) // 150.25
fmt.Println(d.Seconds()) // 9015
fmt.Println(d.Milliseconds()) // 9015000
// Duration 运算
d1 := 1 * time.Hour
d2 := 30 * time.Minute
fmt.Println(d1 + d2) // 1h30m0s
fmt.Println(d1 - d2) // 30m0s
fmt.Println(d1 * 2) // 2h0m0s
fmt.Println(d1 / d2) // 2(整数除法)
// 比较
fmt.Println(d1 > d2) // true
// ParseDuration — 从字符串解析 Duration
d3, _ := time.ParseDuration("1h30m")
fmt.Println(d3) // 1h30m0s
d4, _ := time.ParseDuration("2.5s")
fmt.Println(d4) // 2.5s
d5, _ := time.ParseDuration("100ms")
fmt.Println(d5) // 100ms
}定时器
time.Timer
time.Timer 表示一个单次定时器,在指定时间后向通道发送当前时间。time.After 是其简化版本,time.AfterFunc 在定时触发时执行回调函数。
package main
import (
"fmt"
"time"
)
func main() {
// Timer — 创建单次定时器
timer := time.NewTimer(2 * time.Second)
// 阻塞等待定时器触发
<-timer.C
fmt.Println("2 秒后触发")
// Timer 可以 Stop 和 Reset
timer2 := time.NewTimer(5 * time.Second)
go func() {
<-timer2.C
fmt.Println("5 秒后触发")
}()
// 提前停止
stopped := timer2.Stop()
fmt.Println("停止成功:", stopped) // true
// Reset — 重置定时器
timer3 := time.NewTimer(10 * time.Second)
timer3.Reset(1 * time.Second) // 重置为 1 秒后
<-timer3.C
fmt.Println("1 秒后触发(重置后)")
timer3.Stop()
// After — 便捷函数,返回只读通道
<-time.After(1 * time.Second)
fmt.Println("After: 1 秒后触发")
// AfterFunc — 定时触发回调
timer4 := time.AfterFunc(2*time.Second, func() {
fmt.Println("AfterFunc: 回调被执行")
})
defer timer4.Stop()
time.Sleep(3 * time.Second)
}Timer.Stop 的返回值
Stop() 返回 bool:
true:定时器已停止,在触发前被成功取消false:定时器已过期或已被停止
如果返回 false 但你仍想消费通道中的值,需要先排空通道 <-timer.C,否则可能造成 goroutine 泄漏。
time.Ticker
time.Ticker 表示一个周期性定时器,每隔固定时间向通道发送一次当前时间。适合实现心跳、定期轮询、限速等场景。
package main
import (
"fmt"
"time"
)
func main() {
// Ticker — 创建周期性定时器
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
// 限制执行次数
count := 0
for t := range ticker.C {
count++
fmt.Printf("第 %d 次触发: %v\n", count, t.Format("15:04:05.000"))
if count >= 5 {
break
}
}
// 第 1 次触发: 10:30:00.501
// 第 2 次触发: 10:30:01.001
// 第 3 次触发: 10:30:01.501
// ...
}
// 实际应用:定期检查状态
func periodicCheck() {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
timeout := time.After(15 * time.Second)
for {
select {
case <-ticker.C:
fmt.Println("执行定期检查...")
// 执行检查逻辑
case <-timeout:
fmt.Println("超时,停止检查")
return
}
}
}
// 实际应用:限速器
func rateLimiter() {
// 每秒最多处理 3 个请求
limiter := time.NewTicker(333 * time.Millisecond) // ~3次/秒
defer limiter.Stop()
requests := 10
processed := 0
for i := 0; i < requests; i++ {
<-limiter.C // 等待下一个时间窗口
processed++
fmt.Printf("处理请求 %d\n", i+1)
}
fmt.Printf("共处理 %d 个请求\n", processed)
}务必调用 Stop()
Timer 和 Ticker 都会启动一个后台 goroutine。如果不调用 Stop(),即使通道不再被消费,这个 goroutine 也不会被回收,导致 goroutine 泄漏。始终使用 defer timer.Stop() 和 defer ticker.Stop()。
时区处理
package main
import (
"fmt"
"time"
)
func main() {
// time.Local — 本地时区
now := time.Now()
fmt.Println("本地:", now) // 2024-03-15 10:30:00 +0800 CST
fmt.Println("UTC:", now.UTC()) // 2024-03-15 02:30:00 +0000 UTC
// LoadLocation — 加载指定时区
// 时区名称来自 IANA 时区数据库
loc, err := time.LoadLocation("America/New_York")
if err != nil {
panic(err)
}
nyTime := now.In(loc)
fmt.Println("纽约:", nyTime) // 2024-03-14 22:30:00 -0400 EDT
// 常用时区
tokyo, _ := time.LoadLocation("Asia/Tokyo")
london, _ := time.LoadLocation("Europe/London")
fmt.Println("东京:", now.In(tokyo)) // 2024-03-15 03:30:00 +0900 JST
fmt.Println("伦敦:", now.In(london)) // 2024-03-15 02:30:00 +0000 GMT
// In — 将时间转换到指定时区
utc := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
fmt.Println("UTC:", utc)
fmt.Println("北京:", utc.In(time.FixedZone("CST", 8*3600)))
// FixedZone — 自定义固定偏移时区
// 例如,某些地区不遵循夏令时,使用固定偏移
cst := time.FixedZone("CST", 8*3600) // UTC+8
fmt.Println("固定 UTC+8:", utc.In(cst))
// 时区转换的实际应用
fmt.Println("\n--- 全球会议时间 ---")
fmt.Printf("北京: %s\n", now.In(time.FixedZone("UTC+8", 8*3600)).Format("15:04"))
fmt.Printf("东京: %s\n", now.In(time.FixedZone("UTC+9", 9*3600)).Format("15:04"))
fmt.Printf("伦敦: %s\n", now.In(time.FixedZone("UTC+0", 0)).Format("15:04"))
fmt.Printf("纽约: %s\n", now.In(time.FixedZone("UTC-5", -5*3600)).Format("15:04"))
}存储时间用 UTC
最佳实践:在数据库和 API 中存储/传输时间时,统一使用 UTC 时间。显示给用户时再转换为本地时区。这样可以避免时区混淆和夏令时问题。
性能计时
package main
import (
"fmt"
"time"
)
func main() {
// time.Since — 计算从某个时间点到现在的时间差
start := time.Now()
// 模拟耗时操作
time.Sleep(500 * time.Millisecond)
elapsed := time.Since(start)
fmt.Printf("耗时: %v\n", elapsed) // 耗时: 500.123ms
fmt.Printf("耗时(毫秒): %.2f\n", float64(elapsed)/float64(time.Millisecond))
// time.Until — 计算从现在到某个时间点的时间差
deadline := time.Now().Add(10 * time.Second)
remaining := time.Until(deadline)
fmt.Printf("剩余: %v\n", remaining) // 剩余: 10s
// 更精确的计时(使用 time.Now().Sub)
start2 := time.Now()
// ... 执行代码 ...
elapsed2 := time.Now().Sub(start2)
fmt.Printf("精确耗时: %v\n", elapsed2)
// 实际应用:测量函数执行时间
measureFunc(func() {
time.Sleep(200 * time.Millisecond)
})
}
// 测量函数执行时间的辅助函数
func measureFunc(f func()) {
start := time.Now()
f()
fmt.Printf("函数执行时间: %v\n", time.Since(start))
}练习题
练习 1:倒计时器
编写一个函数 Countdown(duration time.Duration),每秒打印剩余时间,最后打印 “时间到!”。
解题思路:使用 time.NewTicker 每秒触发,记录截止时间,每次触发计算剩余时间。
代码:
package main
import (
"fmt"
"time"
)
func Countdown(duration time.Duration) {
deadline := time.Now().Add(duration)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
remaining := time.Until(deadline)
if remaining <= 0 {
fmt.Println("时间到!")
return
}
select {
case <-ticker.C:
seconds := int(remaining.Seconds())
fmt.Printf("剩余: %02d:%02d:%02d\n",
seconds/3600, (seconds%3600)/60, seconds%60)
}
}
}
func main() {
fmt.Println("10 秒倒计时开始:")
Countdown(10 * time.Second)
}输出:
10 秒倒计时开始:
剩余: 00:00:09
剩余: 00:00:08
...
剩余: 00:00:01
剩余: 00:00:00
时间到!关键点:使用 time.Until(deadline) 而非简单的计数器,避免定时器精度偏差导致的累积误差。
练习 2:时间格式转换器
编写一个函数 ConvertTimeFormat(input, fromLayout, toLayout, tz string) (string, error),将一个时区的字符串时间转换为另一个时区的格式化输出。
解题思路:使用 ParseInLocation 在源时区解析,然后 In 转换到目标时区,最后 Format 格式化输出。
代码:
package main
import (
"fmt"
"time"
)
func ConvertTimeFormat(input, fromLayout, toLayout, tz string) (string, error) {
// 加载目标时区
loc, err := time.LoadLocation(tz)
if err != nil {
return "", fmt.Errorf("无效时区 %q: %w", tz, err)
}
// 在源时区解析时间
t, err := time.ParseInLocation(fromLayout, input, loc)
if err != nil {
return "", fmt.Errorf("解析时间失败: %w", err)
}
// 格式化输出
return t.Format(toLayout), nil
}
func main() {
// 北京时间 → 纽约时间
result, err := ConvertTimeFormat(
"2024-03-15 22:00:00",
"2006-01-02 15:04:05",
time.RFC3339,
"America/New_York",
)
if err != nil {
panic(err)
}
fmt.Println("纽约时间:", result)
// 纽约时间: 2024-03-15T10:00:00-04:00
// 纽约时间 → 东京时间
result2, err := ConvertTimeFormat(
"2024-03-15 10:00:00",
"2006-01-02 15:04:05",
"2006年01月02日 15时04分",
"Asia/Tokyo",
)
if err != nil {
panic(err)
}
fmt.Println("东京时间:", result2)
// 东京时间: 2024年03月15日 23时04分
}关键点:ParseInLocation 确保在正确的时区解析时间字符串,避免时区歧义。
练习 3:任务调度器
实现一个简单的定时任务调度器,支持:
- 在指定延迟后执行任务(
AfterTask) - 定期执行任务(
EveryTask) - 取消任务(
CancelTask)
解题思路:封装 time.AfterFunc 和 time.Ticker,使用 context.Context 支持取消。
代码:
package main
import (
"context"
"fmt"
"sync"
"time"
)
type Scheduler struct {
wg sync.WaitGroup
}
// AfterTask — 延迟执行一次性任务
func (s *Scheduler) AfterTask(delay time.Duration, task func()) {
s.wg.Add(1)
time.AfterFunc(delay, func() {
defer s.wg.Done()
task()
})
}
// EveryTask — 定期执行任务,返回取消函数
func (s *Scheduler) EveryTask(interval time.Duration, task func()) (cancel func()) {
ctx, cancel := context.WithCancel(context.Background())
ticker := time.NewTicker(interval)
s.wg.Add(1)
go func() {
defer s.wg.Done()
defer ticker.Stop()
for {
select {
case <-ticker.C:
task()
case <-ctx.Done():
fmt.Println("定期任务已取消")
return
}
}
}()
return cancel
}
// Wait — 等待所有任务完成
func (s *Scheduler) Wait() {
s.wg.Wait()
}
func main() {
sched := Scheduler{}
// 一次性延迟任务
sched.AfterTask(1*time.Second, func() {
fmt.Println("延迟任务 1 完成")
})
sched.AfterTask(2*time.Second, func() {
fmt.Println("延迟任务 2 完成")
})
// 定期任务
cancel := sched.EveryTask(500*time.Millisecond, func() {
fmt.Println("定期任务执行:", time.Now().Format("15:04:05.000"))
})
// 3 秒后取消定期任务
sched.AfterTask(3*time.Second, func() {
cancel()
fmt.Println("已发送取消信号")
})
sched.Wait()
fmt.Println("所有任务完成")
}输出:
延迟任务 1 完成
定期任务执行: 10:30:01.501
定期任务执行: 10:30:02.001
延迟任务 2 完成
定期任务执行: 10:30:02.501
已发送取消信号
定期任务已取消
所有任务完成关键点:使用 context.WithCancel 实现优雅取消,sync.WaitGroup 确保所有任务完成后再退出主 goroutine。
