第34课:时间与定时器
学习目标
通过本课学习,你将能够:
- 理解Go语言中时间的表示与格式化
- 使用
time.Duration进行时间间隔计算 - 掌握
time.Timer实现单次定时任务 - 运用
time.Ticker实现周期性定时任务 - 了解如何在并发编程中安全地使用定时器
核心概念
时间的基本表示
在Go中,time.Time类型表示一个具体的时间点。获取当前时间非常简单:
now := time.Now() // 获取当前本地时间
时间段(Duration)
time.Duration表示两个时间点之间的时间间隔,单位为纳秒。我们常用time.Second、time.Minute等常量:
duration := 5 * time.Second // 5秒的时间段
duration2 := 30 * time.Minute // 30分钟的时间段
定时器(Timer)
想象一下闹钟:你设置一个时间,到时间后它会响。time.Timer就是这个原理,它会在指定时间后向通道发送一个值。
打点器(Ticker)
这个更像心跳,每隔固定时间就会触发一次。time.Ticker会定期向通道发送时间点,适合需要周期性执行的任务。
代码示例
1. 时间基本操作
package main
import (
"fmt"
"time"
)
func main() {
// 获取当前时间
now := time.Now()
fmt.Println("当前时间:", now)
// 格式化输出
// 参考时间:Mon Jan 2 15:04:05 MST 2006
formatted := now.Format("2006-01-02 15:04:05")
fmt.Println("格式化后:", formatted)
// 获取时间的各个部分
year, month, day := now.Date()
hour, min, sec := now.Clock()
fmt.Printf("年:%d 月:%d 日:%d\n", year, month, day)
fmt.Printf("时:%d 分:%d 秒:%d\n", hour, min, sec)
// 时间计算:2小时后
twoHoursLater := now.Add(2 * time.Hour)
fmt.Println("两小时后:", twoHoursLater.Format("15:04:05"))
// 时间差计算
past := now.Add(-30 * time.Minute) // 30分钟前
duration := now.Sub(past)
fmt.Printf("时间差: %v\n", duration) // 输出: 30m0s
}
2. Timer:单次定时
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("开始等待...")
// 创建一个2秒后触发的定时器
timer := time.NewTimer(2 * time.Second)
// 等待定时器触发
<-timer.C // C是定时器的通道,触发时会发送当前时间
fmt.Println("2秒到了!")
// 另一种方式:使用time.After
fmt.Println("再次等待...")
<-time.After(3 * time.Second) // 等待3秒
fmt.Println("又过了3秒!")
// 定时器的取消
timer2 := time.NewTimer(5 * time.Second)
go func() {
<-timer2.C
fmt.Println("这个不应该被打印")
}()
// 立即停止定时器
stopped := timer2.Stop()
if stopped {
fmt.Println("定时器已停止")
}
// 给goroutine一点时间检查
time.Sleep(100 * time.Millisecond)
fmt.Println("程序结束")
}
3. Ticker:周期定时
package main
import (
"fmt"
"time"
)
func main() {
// 创建一个每秒触发一次的打点器
ticker := time.NewTicker(1 * time.Second)
done := make(chan bool)
// 在goroutine中处理打点
go func() {
for {
select {
case t := <-ticker.C:
fmt.Println("当前时间:", t.Format("15:04:05"))
case <-done:
fmt.Println("停止打点器")
return
}
}
}()
// 运行5秒后停止
time.Sleep(5 * time.Second)
ticker.Stop() // 停止打点器
done <- true // 通知goroutine退出
close(done)
// 等待goroutine清理
time.Sleep(100 * time.Millisecond)
fmt.Println("程序结束")
}
实践练习
练习1:时间格式化(简单)
创建一个程序,获取当前时间并按照以下格式输出:
当前日期:2023年12月25日
当前时间:14:30:05
星期几:星期一
预期输出格式:
当前日期:2023年12月25日
当前时间:14:30:05
星期几:星期一
练习2:超时模拟(中等)
使用time.Timer模拟一个可能耗时很长的操作。如果操作在3秒内没有完成,打印"操作超时"并退出;如果操作完成,打印"操作成功完成"。
提示:使用select语句同时监听操作完成通道和定时器通道。
练习3:心跳检测(较难)
创建一个模拟心跳检测的程序:
- 有一个"服务器"每500毫秒发送一次心跳信号
- 有一个"客户端"监听心跳,如果超过2秒没收到心跳,打印"服务器连接丢失"
- 运行5秒后,模拟服务器停止发送心跳
常见错误
1. 忘记启动定时器
// 错误:创建了定时器但没有启动
timer := time.NewTimer(5 * time.Second)
// 需要等待timer.C才能触发
// 正确用法:
timer := time.NewTimer(5 * time.Second)
<-timer.C // 这里才会等待
2. Ticker没有停止导致泄漏
// 错误:没有停止Ticker
ticker := time.NewTicker(1 * time.Second)
go func() {
for t := range ticker.C {
fmt.Println(t)
}
}()
// 程序结束时ticker还在运行,导致内存泄漏
// 正确做法:确保调用Stop()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() // 确保在函数退出时停止
3. 在循环中错误使用time.After
// 问题代码:每次循环都会创建新的Timer
for i := 0; i < 5; i++ {
select {
case <-time.After(1 * time.Second): // 这里每次循环都创建新Timer
fmt.Println("超时")
case data := <-ch:
fmt.Println(data)
}
}
// 改进:在循环外创建Timer,循环内重置
timeout := time.After(5 * time.Second) // 总超时5秒
for i := 0; i < 5; i++ {
select {
case <-timeout:
fmt.Println("总超时")
return
case data := <-ch:
fmt.Println(data)
}
}
4. 混淆UTC和本地时间
now := time.Now() // 本地时间
utc := time.Now().UTC() // UTC时间
// 格式化时注意时区
fmt.Println(now.Format("15:04:05")) // 本地时间格式
fmt.Println(utc.Format("15:04:05")) // UTC时间格式
小结
关键要点回顾
- 时间表示:使用
time.Now()获取当前时间,Format()进行格式化输出 - 时间段:
time.Duration表示时间间隔,支持加减乘除运算 - Timer:单次定时器,通过
NewTimer()创建,监听.C通道获取触发事件 - Ticker:周期打点器,通过
NewTicker()创建,需要及时调用Stop()避免泄漏 - 超时控制:结合
select语句和Timer/Ticker实现超时控制和周期任务
使用技巧
- 定时器通道的读取是非阻塞的(如果还没到时间)
- 在并发程序中,Timer和Ticker的通道可以安全地在多个goroutine间传递
- 使用
time.Sleep()进行简单等待,但定时器更适合复杂的并发场景
下一步学习
下一课我们将学习如何获取和处理命令行参数与环境变量,这是编写实用CLI工具的重要技能。
练习编辑器
go
Loading...