Go 从 0 到精通 · 第 20 课:时间处理

学习定位:这是整套 Go 教程的第 20 课。
前置要求:已经完成第 19 课,掌握了文件操作基础。需要熟悉基本结构体和方法的用法。
本课目标:掌握 Go 中时间与日期的常见操作,包括获取时间、格式化、解析、时间计算、定时器和超时控制,能处理日志时间、延迟和超时等真实场景。


1. 本课你要解决的核心问题

时间无处不在——日志要带时间戳,缓存要判断过期,任务要定时执行,接口要设置超时……

你需要搞明白以下问题:

  • 怎么获取当前时间
  • Go 独特的时间格式化方式到底怎么回事
  • 怎么把字符串解析成时间、把时间转成字符串
  • 怎么做时间加减、比较
  • 怎么实现定时器和周期执行
  • 怎么设置超时控制

学完这一课,你就能让程序"感知时间"了。


2. time 包总览

Go 的所有时间功能都在 time 包里。核心类型就两个:

类型 含义
time.Time 一个时间点(比如 “2024年1月15日 10点30分0秒”)
time.Duration 一段时间(比如 “3秒”、“5分钟”)

先记住这两个概念,下面逐步展开。


3. 获取当前时间

3.1 time.Now()

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
"time"
)

func main() {
now := time.Now()
fmt.Println(now)
}

输出(取决于你的时区):

1
2024-01-15 10:30:45.123456789 +0800 CST

这个输出包含:

  • 2024-01-15:日期
  • 10:30:45.123456789:时间(精确到纳秒)
  • +0800:时区偏移(东8区)
  • CST:时区名称

3.2 获取时间的各个组成部分

time.Time 提供了一系列方法来获取年、月、日、时、分、秒:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"time"
)

func main() {
now := time.Now()

fmt.Println("年:", now.Year()) // 2024
fmt.Println("月:", now.Month()) // January(英文)
fmt.Println("月份数字:", int(now.Month())) // 1
fmt.Println("日:", now.Day()) // 15
fmt.Println("时:", now.Hour()) // 10
fmt.Println("分:", now.Minute()) // 30
fmt.Println("秒:", now.Second()) // 45
fmt.Println("纳秒:", now.Nanosecond()) // 123456789
fmt.Println("星期几:", now.Weekday()) // Monday
}

注意now.Month() 返回的是 time.Month 类型,打印出来是英文名。要数字就 int(now.Month())

3.3 获取更多时间信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
now := time.Now()

// 一年中的第几天(1-366)
fmt.Println("一年中的第几天:", now.YearDay())

// 当天从0点开始过了多少时间
year, month, day := now.Date()
midnight := time.Date(year, month, day, 0, 0, 0, 0, now.Location())
since := now.Sub(midnight)
fmt.Println("今天已过:", since)

// 是否是闰年(通过判断2月天数)
isLeap := time.Date(now.Year(), 3, 0, 0, 0, 0, 0, time.UTC).Day() == 29
fmt.Println("闰年:", isLeap)

4. 时间格式化——Go 最独特的设计之一

4.1 为什么说它"独特"

其他语言用占位符格式化时间:

1
2
3
Java:     yyyy-MM-dd HH:mm:ss
Python: %Y-%m-%d %H:%M:%S
C: %Y-%m-%d %H:%M:%S

Go 用一个特定的参考时间来格式化:

1
2006-01-02 15:04:05 Monday MST -0700

记住这个时间:2006年1月2日 15点4分5秒 星期一 MST -0700

为什么选这个时间?因为这是 Go 语言正式对外公布的时间(2006年1月2日下午3点4分5秒,美国山地标准时间-0700)。每个数字都是唯一的:

位置 含义
2006 四位年份
01 两位月份(01-12)
02 两位日期(01-31)
15 24小时制(00-23)
04 分钟(00-59)
05 秒(00-59)
Monday 星期 星期几
MST 时区 时区缩写
-0700 时区偏移 ±HHMM

如何记住这个参考时间:1-2-3-4-5-6。即 2006-01-02 15:04:05。月是01,日是02,时是15(下午3点=12+3),分是04,秒是05。

4.2 用 Format 格式化时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"time"
)

func main() {
now := time.Now()

// 常用格式
fmt.Println(now.Format("2006-01-02"))
// 输出:2024-01-15

fmt.Println(now.Format("2006-01-02 15:04:05"))
// 输出:2024-01-15 10:30:45

fmt.Println(now.Format("2006/01/02 15:04"))
// 输出:2024/01/15 10:30

fmt.Println(now.Format("01-02 15:04"))
// 输出:01-15 10:30

fmt.Println(now.Format("15:04:05"))
// 输出:10:30:45

// 带星期
fmt.Println(now.Format("2006-01-02 Monday"))
// 输出:2024-01-15 Monday

// 12小时制
fmt.Println(now.Format("2006-01-02 03:04:05 PM"))
// 输出:2024-01-15 10:30:45 AM

// RFC3339 标准格式(国际通用)
fmt.Println(now.Format(time.RFC3339))
// 输出:2024-01-15T10:30:45+08:00
}

4.3 Go 预定义的格式常量

Go 提供了一批预定义的格式,避免你每次都手写参考时间:

1
2
3
4
5
6
// 常用预定义格式
time.ANSIC // "Mon Jan _2 15:04:05 2006"
time.RFC3339 // "2006-01-02T15:04:05Z07:00" ← 国际标准,推荐
time.RFC822 // "02 Jan 06 15:04 MST"
time.Kitchen // "3:04PM"
time.Stamp // "Jan _2 15:04:05"

使用方式:

1
now.Format(time.RFC3339)

4.4 格式化实战:日志时间戳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"time"
)

func log(level, msg string) {
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
fmt.Printf("[%s] [%-5s] %s\n", timestamp, level, msg)
}

func main() {
log("INFO", "程序启动")
log("DEBUG", "加载配置文件")
log("INFO", "监听端口 8080")
log("WARN", "配置文件未找到,使用默认值")
log("ERROR", "数据库连接失败")
}

输出:

1
2
3
4
5
[2024-01-15 10:30:45.123] [INFO ] 程序启动
[2024-01-15 10:30:45.123] [DEBUG] 加载配置文件
[2024-01-15 10:30:45.124] [INFO ] 监听端口 8080
[2024-01-15 10:30:45.124] [WARN ] 配置文件未找到,使用默认值
[2024-01-15 10:30:45.124] [ERROR] 数据库连接失败

5. 解析时间字符串

5.1 用 time.Parse 把字符串变成 time.Time

解析和格式化是相反的过程。格式化用的参考时间,解析也要用同样的参考时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"time"
)

func main() {
// 解析 "2006-01-02 15:04:05" 格式的时间字符串
t, err := time.Parse("2006-01-02 15:04:05", "2024-03-15 14:30:00")
if err != nil {
fmt.Println("解析失败:", err)
return
}

fmt.Println("解析结果:", t)
fmt.Println("年:", t.Year())
fmt.Println("月:", t.Month())
fmt.Println("日:", t.Day())
fmt.Println("是星期几:", t.Weekday())
}

输出:

1
2
3
4
5
解析结果: 2024-03-15 14:30:00 +0000 UTC
年: 2024
月: March
日: 15
是星期几: Friday

注意time.Parse 解析出的时间默认是 UTC 时区(+0000 UTC),不是你本地时区!要用 time.ParseInLocation 来指定时区(下面讲)。

5.2 解析不同格式的时间字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 只有日期
t, _ := time.Parse("2006-01-02", "2024-03-15")
fmt.Println(t) // 2024-03-15 00:00:00 +0000 UTC

// 只有时间(日期部分为 0001-01-01)
t, _ := time.Parse("15:04:05", "14:30:00")
fmt.Println(t) // 0001-01-01 14:30:00 +0000 UTC

// 斜杠分隔
t, _ := time.Parse("2006/01/02", "2024/03/15")
fmt.Println(t) // 2024-03-15 00:00:00 +0000 UTC

// 带时区偏移
t, _ := time.Parse("2006-01-02T15:04:05-07:00", "2024-03-15T14:30:00+08:00")
fmt.Println(t) // 2024-03-15 14:30:00 +0800 +0800

5.3 time.ParseInLocation:指定时区解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"time"
)

func main() {
// 加载上海时区(东8区)
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
fmt.Println("加载时区失败:", err)
return
}

// 指定时区解析
t, err := time.ParseInLocation("2006-01-02 15:04:05", "2024-03-15 14:30:00", loc)
if err != nil {
fmt.Println("解析失败:", err)
return
}

fmt.Println("解析结果:", t)
// 输出:2024-03-15 14:30:00 +0800 CST
}

这和 time.Parse 的区别

  • time.Parse("...","2024-03-15 14:30:00")2024-03-15 14:30:00 +0000 UTC
  • time.ParseInLocation("...","2024-03-15 14:30:00", shanghai)2024-03-15 14:30:00 +0800 CST

同样一个字符串,解析出的时刻是不同的!前者认为是 UTC 时间,后者认为是东8区时间。

1
2
UTC 版本:   2024-03-15 14:30:00 UTC   = 2024-03-15 22:30:00 北京时间
上海版本: 2024-03-15 14:30:00 +0800 = 2024-03-15 14:30:00 北京时间

实际开发中:解析用户输入的时间(比如 “2024-03-15 14:30”),几乎总是要用 ParseInLocation,因为用户说的时间通常是本地时区。

5.4 常用时区

1
2
3
4
5
6
7
8
// 加载时区
loc, _ := time.LoadLocation("Asia/Shanghai") // 东8区,北京时间
loc, _ := time.LoadLocation("America/New_York") // 美国东部
loc, _ := time.LoadLocation("UTC") // 协调世界时
loc, _ := time.LoadLocation("Local") // 本地时区

// 获取本地时区
local := time.Now().Location()

6. 时间计算

6.1 time.Duration:时间段

time.Duration 表示一段时间,本质是一个 int64(纳秒数)。Go 提供了方便的常量:

1
2
3
4
5
6
time.Nanosecond   // 1 纳秒
time.Microsecond // 1000 纳秒
time.Millisecond // 1000 微秒
time.Second // 1 秒
time.Minute // 60 秒
time.Hour // 3600 秒

6.2 时间加减

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"time"
)

func main() {
now := time.Now()

// 加 1 小时
later := now.Add(1 * time.Hour)
fmt.Println("1小时后:", later.Format("15:04:05"))

// 减 30 分钟
earlier := now.Add(-30 * time.Minute)
fmt.Println("30分钟前:", earlier.Format("15:04:05"))

// 加 2 天
in2Days := now.Add(48 * time.Hour)
fmt.Println("2天后:", in2Days.Format("2006-01-02"))

// 减 1 周
lastWeek := now.Add(-7 * 24 * time.Hour)
fmt.Println("1周前:", lastWeek.Format("2006-01-02"))
}

输出示例:

1
2
3
4
1小时后:  11:30:45
30分钟前: 10:00:45
2天后: 2024-01-17
1周前: 2024-01-08

6.3 更安全的时间加减:AddDate

Add 加天数需要自己算小时数(24 * time.Hour),而且处理跨月、闰年很麻烦。用 AddDate 更安全:

1
2
3
4
5
6
7
8
9
10
11
12
13
now := time.Now()

// 加 1 年
fmt.Println(now.AddDate(1, 0, 0).Format("2006-01-02"))

// 加 2 个月
fmt.Println(now.AddDate(0, 2, 0).Format("2006-01-02"))

// 减 10 天
fmt.Println(now.AddDate(0, 0, -10).Format("2006-01-02"))

// 加 1 年 3 个月 5 天
fmt.Println(now.AddDate(1, 3, 5).Format("2006-01-02"))

AddDate(years, months, days) 能正确处理月末和闰年:

1
2
3
4
5
6
7
8
// 2024-01-31 加 1 个月
t := time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC)
fmt.Println(t.AddDate(0, 1, 0))
// 输出:2024-02-29(2024是闰年,自动调整到2月最后一天)

// 2024-01-31 加 2 个月
fmt.Println(t.AddDate(0, 2, 0))
// 输出:2024-03-31(3月有31天,正常)

6.4 计算两个时间之间的差值:Sub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"time"
)

func main() {
start := time.Now()

// 模拟一些操作
time.Sleep(1500 * time.Millisecond)

end := time.Now()

// 计算差值
duration := end.Sub(start)
fmt.Println("耗时:", duration) // 1.5001234s
fmt.Println("耗时(秒):", duration.Seconds()) // 1.5001234
fmt.Println("耗时(毫秒):", duration.Milliseconds()) // 1500
fmt.Println("耗时(微秒):", duration.Microseconds()) // 1500123
fmt.Println("耗时(纳秒):", duration.Nanoseconds()) // 1500123400
}

Sub 返回的是 time.Duration,可以用各种方法转换成不同单位。

6.5 实战:函数执行计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"time"
)

// 用 time.Since 快捷计算"从某个时间到现在过了多久"
func slowOperation() {
start := time.Now()

// 模拟耗时操作
total := 0
for i := 0; i < 10000000; i++ {
total += i
}

elapsed := time.Since(start) // 等价于 time.Now().Sub(start)
fmt.Printf("操作完成,耗时 %v\n", elapsed)
}

func main() {
slowOperation()
}

time.Since(t)time.Now().Sub(t) 的简写,非常常用。

6.6 time.Until:到某个时间还有多久

1
2
3
deadline := time.Date(2024, 12, 31, 23, 59, 59, 0, time.Local)
remaining := time.Until(deadline) // 等价于 deadline.Sub(time.Now())
fmt.Printf("距离年底还有 %v\n", remaining)

7. 时间比较

7.1 BeforeAfterEqual

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"time"
)

func main() {
t1 := time.Date(2024, 3, 15, 10, 0, 0, 0, time.UTC)
t2 := time.Date(2024, 3, 15, 14, 0, 0, 0, time.UTC)

fmt.Println("t1 在 t2 之前?", t1.Before(t2)) // true
fmt.Println("t1 在 t2 之后?", t1.After(t2)) // false
fmt.Println("t1 和 t2 相等?", t1.Equal(t2)) // false

// 同一个时刻
t3 := time.Date(2024, 3, 15, 10, 0, 0, 0, time.UTC)
fmt.Println("t1 和 t3 相等?", t1.Equal(t3)) // true
}

注意:比较时间用 Equal,不要用 ==。因为 time.Time 内部可能包含不同的时区信息,== 会比较所有字段,可能意外返回 false

7.2 判断时间是否在某个区间内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"time"
)

func main() {
now := time.Now()
start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.Local)
end := time.Date(2024, 12, 31, 23, 59, 59, 0, time.Local)

if now.After(start) && now.Before(end) {
fmt.Println("当前时间在 2024 年内")
} else {
fmt.Println("当前时间不在 2024 年内")
}
}

7.3 判断是否是今天

1
2
3
4
5
6
func isToday(t time.Time) bool {
now := time.Now()
y1, m1, d1 := t.Date()
y2, m2, d2 := now.Date()
return y1 == y2 && m1 == m2 && d1 == d2
}

8. 时区处理

8.1 time.UTCtime.Local

1
2
3
4
5
6
7
8
9
10
11
now := time.Now()

// 转换为 UTC
utc := now.UTC()
fmt.Println("本地时间:", now)
fmt.Println("UTC 时间:", utc)

// 转换为指定时区
loc, _ := time.LoadLocation("America/New_York")
nyTime := now.In(loc)
fmt.Println("纽约时间:", nyTime)

8.2 时间戳

Unix 时间戳是从 1970-01-01 00:00:00 UTC 到现在的秒数(或纳秒数)。这是很多系统和数据库交互的通用格式:

1
2
3
4
5
6
7
8
9
10
11
12
now := time.Now()

// 时间 → 时间戳
fmt.Println("秒级时间戳:", now.Unix()) // 1705285845
fmt.Println("毫秒级时间戳:", now.UnixMilli()) // 1705285845123
fmt.Println("微秒级时间戳:", now.UnixMicro()) // 1705285845123456
fmt.Println("纳秒级时间戳:", now.UnixNano()) // 1705285845123456789

// 时间戳 → 时间
ts := int64(1705285845)
t := time.Unix(ts, 0) // 第一个参数是秒,第二个是额外的纳秒
fmt.Println("从时间戳还原:", t.Format("2006-01-02 15:04:05"))

8.3 实战:数据库时间戳处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"time"
)

func main() {
// 模拟数据库中存储的秒级时间戳
dbTimestamp := int64(1705285845)

// 时间戳 → time.Time
t := time.Unix(dbTimestamp, 0)
fmt.Println("数据库时间:", t.Format("2006-01-02 15:04:05"))

// 存入数据库:time.Time → 秒级时间戳
now := time.Now()
fmt.Println("存入时间戳:", now.Unix())

// 也可以用毫秒级(更高精度)
fmt.Println("存入毫秒:", now.UnixMilli())
}

9. 定时器与休眠

9.1 time.Sleep:暂停执行

最简单的"等待"操作:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"time"
)

func main() {
fmt.Println("开始等待...")
time.Sleep(3 * time.Second)
fmt.Println("3秒过去了!")
}

time.Sleep 会阻塞当前 goroutine,参数是 time.Duration

1
2
3
time.Sleep(500 * time.Millisecond)  // 等 500 毫秒
time.Sleep(2 * time.Minute) // 等 2 分钟
time.Sleep(1 * time.Hour) // 等 1 小时

9.2 time.Tick:周期性触发(定时器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"time"
)

func main() {
// 每秒触发一次
ticker := time.Tick(1 * time.Second)

count := 0
for t := range ticker {
count++
fmt.Printf("[%s] 第 %d 次触发\n", t.Format("15:04:05"), count)
if count >= 5 {
break
}
}

fmt.Println("结束")
}

输出:

1
2
3
4
5
6
[10:30:01]1 次触发
[10:30:02]2 次触发
[10:30:03]3 次触发
[10:30:04]4 次触发
[10:30:05]5 次触发
结束

注意time.Tick 返回的 channel 永远不会关闭,不能用于需要停止的场景。如果需要停止定时器,用 time.NewTicker(下面讲)。

9.3 time.After:一次性延迟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"time"
)

func main() {
fmt.Println("等待 2 秒...")

// time.After 返回一个 channel,2 秒后会收到一个值
<-time.After(2 * time.Second)

fmt.Println("2 秒到了!")
}

time.After 常用于超时控制(后面会详细讲)。

9.4 time.NewTicker:可控制的定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"time"
)

func main() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() // 重要:用完要停止,否则 goroutine 泄漏

count := 0
for {
<-ticker.C
count++
fmt.Printf("第 %d 次\n", count)
if count >= 5 {
break
}
}
}

NewTickerTick 的区别:

time.Tick time.NewTicker
能否停止 不能(channel 永不关闭) 能(调用 ticker.Stop()
适用场景 简单一次性场景 需要控制生命周期的场景
内存泄漏风险 有(不用时仍占资源) 无(Stop 后释放)

经验法则:在生产代码中,永远用 NewTicker,不要用 Tick

9.5 time.NewTimer:单次延迟触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"time"
)

func main() {
timer := time.NewTimer(2 * time.Second)
defer timer.Stop()

fmt.Println("等待 2 秒...")

<-timer.C
fmt.Println("时间到!")
}

TimerTicker 的区别:

  • Timer:触发一次就结束
  • Ticker:周期性触发

9.6 实战:倒计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"time"
)

func countdown(seconds int) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

for i := seconds; i > 0; i-- {
fmt.Printf("\r倒计时: %d 秒 ", i)
<-ticker.C
}
fmt.Println("\r倒计时结束! ")
}

func main() {
fmt.Println("准备发射火箭!")
countdown(5)
fmt.Println("火箭已发射!")
}

10. 超时控制

10.1 用 select + time.After 实现超时

这是最经典的 Go 超时模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"time"
)

func slowTask() string {
time.Sleep(3 * time.Second) // 模拟耗时操作
return "任务完成"
}

func main() {
resultCh := make(chan string, 1)

// 在 goroutine 中执行耗时任务
go func() {
resultCh <- slowTask()
}()

// 等待结果或超时
select {
case result := <-resultCh:
fmt.Println("收到结果:", result)
case <-time.After(2 * time.Second):
fmt.Println("超时!任务耗时太长")
}
}

输出:

1
超时!任务耗时太长

这里 slowTask 需要 3 秒,但我们只等 2 秒就超时了。

10.2 实战:带超时的 HTTP 请求模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
"fmt"
"time"
)

// 模拟一个可能很慢的网络请求
func fetchURL(url string) string {
delay := time.Duration(1+time.Now().UnixNano()%3) * time.Second
fmt.Printf("模拟请求 %s,预计耗时 %v\n", url, delay)
time.Sleep(delay)
return fmt.Sprintf("来自 %s 的响应", url)
}

func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
resultCh := make(chan string, 1)

go func() {
resultCh <- fetchURL(url)
}()

select {
case result := <-resultCh:
return result, nil
case <-time.After(timeout):
return "", fmt.Errorf("请求 %s 超时(超过 %v)", url, timeout)
}
}

func main() {
urls := []string{
"https://api.example.com/users",
"https://api.example.com/orders",
"https://api.example.com/products",
}

for _, url := range urls {
result, err := fetchWithTimeout(url, 2*time.Second)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("成功:", result)
}
fmt.Println()
}
}

11. 综合示例:简易定时提醒工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"fmt"
"time"
)

// Reminder 定时提醒
type Reminder struct {
Message string
At time.Time
}

func main() {
now := time.Now()

// 设置几个提醒
reminders := []Reminder{
{
Message: "该喝水了!",
At: now.Add(5 * time.Second),
},
{
Message: "站起来活动一下!",
At: now.Add(10 * time.Second),
},
{
Message: "休息一下眼睛!",
At: now.Add(15 * time.Second),
},
}

fmt.Println("定时提醒已设置:")
for _, r := range reminders {
fmt.Printf(" %s -> %s\n", r.At.Format("15:04:05"), r.Message)
}
fmt.Println()

// 逐个等待并触发提醒
for _, r := range reminders {
wait := time.Until(r.At)
if wait > 0 {
time.Sleep(wait)
}
fmt.Printf("[%s] %s\n",
time.Now().Format("15:04:05"), r.Message)
}

fmt.Println("所有提醒完毕!")
}

输出:

1
2
3
4
5
6
7
8
9
定时提醒已设置:
10:30:50 -> 该喝水了!
10:30:55 -> 站起来活动一下!
10:31:00 -> 休息一下眼睛!

[10:30:50] 该喝水了!
[10:30:55] 站起来活动一下!
[10:31:00] 休息一下眼睛!
所有提醒完毕!

12. 常见坑总结

12.1 格式化参考时间写错

最常见的错误:把参考时间写成自己习惯的格式,而不是 Go 的固定参考时间。

1
2
3
4
5
6
// 错误:这不是 Go 的参考时间
now.Format("2024-01-15 10:30:45") // 输出完全错误!
now.Format("YYYY-MM-DD HH:mm:ss") // 输出完全错误!

// 正确:用 Go 的参考时间
now.Format("2006-01-02 15:04:05") // 正确

12.2 time.Parse 不理解时区

1
2
3
4
5
6
7
8
// Parse 默认解析为 UTC
t, _ := time.Parse("2006-01-02 15:04:05", "2024-03-15 14:30:00")
fmt.Println(t.Location()) // UTC(不是本地时区!)

// 用户输入的时间应该用 ParseInLocation
loc, _ := time.LoadLocation("Asia/Shanghai")
t, _ = time.ParseInLocation("2006-01-02 15:04:05", "2024-03-15 14:30:00", loc)
fmt.Println(t.Location()) // Asia/Shanghai

12.3 用 == 比较时间

1
2
3
4
5
6
7
8
t1 := time.Now()
t2 := time.Now()

// 错误:可能有纳秒差异或时区差异
if t1 == t2 { ... } // 不可靠!

// 正确
if t1.Equal(t2) { ... }

12.4 time.Tick 导致 goroutine 泄漏

1
2
3
4
5
6
7
8
9
10
11
12
// 错误:Tick 的 channel 永远不会关闭,资源不会释放
func doSomething() {
tick := time.Tick(1 * time.Second)
// 用完后 tick 仍在后台运行
}

// 正确:用 NewTicker + Stop
func doSomething() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
// 函数退出时资源释放
}

12.5 加减天数用 Add 而不是 AddDate

1
2
3
4
5
// 能工作,但跨夏令时可能出错(某些时区)
t.Add(24 * time.Hour)

// 更安全:正确处理月末、闰年、夏令时
t.AddDate(0, 0, 1)

严格来说 Add(24 * time.Hour) 在大多数情况下没问题,但 AddDate 语义更明确,处理边界情况更安全。

12.6 忘记 timer.Stop()ticker.Stop()

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在循环中创建 ticker
for {
ticker := time.NewTicker(1 * time.Second)
// 每次循环都创建新的 ticker,旧的没有 Stop
// → goroutine 和 channel 泄漏!
}

// 正确:在循环外创建,或确保 Stop
for {
ticker := time.NewTicker(1 * time.Second)
// 处理逻辑...
ticker.Stop()
}

12.7 混淆 time.Aftertime.NewTimer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// time.After 每次调用都创建新的 timer,频繁调用会泄漏
for {
select {
case <-ch:
// 处理
case <-time.After(5 * time.Second): // 每次循环都创建新 timer!
// 超时
}
}

// 正确:用 NewTimer,在循环外创建
timer := time.NewTimer(5 * time.Second)
for {
timer.Reset(5 * time.Second)
select {
case <-ch:
// 处理
case <-timer.C:
// 超时
}
}

12.8 time.Parse 格式字符串和输入字符串不匹配

1
2
3
4
5
6
// 格式是 "2006-01-02 15:04:05",但输入没有秒
t, err := time.Parse("2006-01-02 15:04:05", "2024-03-15 14:30")
// err: parsing time "2024-03-15 14:30" as "2006-01-02 15:04:05": ...

// 正确:格式要和输入一致
t, err := time.Parse("2006-01-02 15:04", "2024-03-15 14:30")

13. time 包常用函数速查表

函数 用途 示例
time.Now() 获取当前时间 now := time.Now()
time.Parse(layout, str) 解析时间字符串(UTC) t, _ := time.Parse("2006-01-02", "2024-03-15")
time.ParseInLocation(layout, str, loc) 解析时间字符串(指定时区) t, _ := time.ParseInLocation("...", str, loc)
time.Date(y,m,d,h,m,s,ns,loc) 构造时间 t := time.Date(2024,3,15,0,0,0,0,time.UTC)
time.Unix(sec, nsec) 时间戳 → 时间 t := time.Unix(1705285845, 0)
time.Sleep(d) 暂停 time.Sleep(time.Second)
time.Tick(d) 周期触发(不推荐) for t := range time.Tick(time.Second)
time.After(d) 延迟后触发 <-time.After(time.Second)
time.Since(t) 从 t 到现在过了多久 time.Since(start)
time.Until(t) 从现在到 t 还有多久 time.Until(deadline)
time.LoadLocation(name) 加载时区 loc, _ := time.LoadLocation("Asia/Shanghai")

14. 本课练习

练习 1:格式化与解析

要求:

  • 获取当前时间,格式化为 "2006年01月02日 15:04:05" 的中文格式
  • 解析字符串 "2024/03/15",打印出是星期几
  • 将当前时间转换为时间戳(秒级),再转换回来验证

练习 2:时间计算

要求:

  • 计算从你出生到现在过了多少天
  • 给定一个日期,计算下个月的同一天是几号(注意月末边界)
  • 计算今天距离本年底还有多少天

练习 3:简易计时器

要求:

  • 写一个函数,接收秒数,显示倒计时
  • 倒计时结束时播放提示(打印 "时间到!"
  • time.NewTicker 实现

练习 4:超时控制

要求:

  • 模拟一个随机耗时的操作(随机 1~5 秒)
  • 设置 3 秒超时
  • 如果在超时内完成,打印结果和耗时
  • 如果超时,打印超时信息

练习 5:日志时间解析

要求:

  • 写一个函数解析日志文件中的时间戳(格式:[2024-01-15 10:30:45]
  • 找出最早的和最晚的日志条目
  • 计算日志跨越了多长时间

15. 自测题

15.1 概念题

  1. time.Now() 返回什么类型的值?
  2. Go 的时间格式化参考时间是什么?为什么选这个时间?
  3. time.Parsetime.ParseInLocation 有什么区别?
  4. time.Duration 本质是什么类型?
  5. time.Addtime.AddDate 各自适合什么场景?
  6. time.Sincetime.Until 分别等价于什么?
  7. time.Ticktime.NewTicker 有什么区别?为什么推荐用后者?
  8. 比较两个时间是否相等应该用 == 还是 Equal
  9. 怎么把 time.Time 转换成 Unix 时间戳?
  10. time.After 常用于什么场景?

15.2 代码阅读题

下面的格式化输出是什么?

1
2
3
4
5
6
t := time.Date(2024, 3, 15, 14, 5, 3, 0, time.UTC)

fmt.Println(t.Format("2006-01-02"))
fmt.Println(t.Format("15:04:05"))
fmt.Println(t.Format("2006/1/2 3:4:5 PM"))
fmt.Println(t.Format("Monday"))
点击查看答案
1
2
3
4
2024-03-15
14:05:03
2024/3/15 2:5:3 PM
Friday

解释:

  • "2006-01-02":年-月-日,月和日用两位补零
  • "15:04:05":24小时制的时:分:秒
  • "2006/1/2 3:4:5 PM":不补零的日期 + 12小时制 + AM/PM
  • "Monday":星期几的英文名(2024-03-15 是星期五)

16. 本课总结

这一课你学到了 Go 中时间处理的完整知识。

你现在应该已经理解:

场景 推荐方式
获取当前时间 time.Now()
格式化输出 t.Format("2006-01-02 15:04:05")
解析时间字符串 time.Parsetime.ParseInLocation
时间加减 Add(duration)AddDate(y, m, d)
计算时间差 t1.Sub(t2)time.Since(t)
时间比较 BeforeAfterEqual(不用 ==
时区转换 t.In(loc)
时间戳转换 Unix() / time.Unix(ts, 0)
暂停执行 time.Sleep
周期执行 time.NewTicker(记得 Stop
超时控制 select + time.After

最重要的三件事:

  1. 格式化用 "2006-01-02 15:04:05",不要写成其他格式
  2. 解析用户输入的时间用 ParseInLocation,不用 Parse
  3. 比较时间用 Equal,不用 ==

17. 下一课预告

下一课我们学习字符串处理与常用工具库

会重点讲:

  • strings 包:拼接、拆分、查找、替换
  • strconv 包:字符串与数字互转
  • unicode 包:字符判断
  • 常见文本清洗和转换场景

学完下一课,你就能高效处理各种文本数据了。