34·标准库入门

时间与定时器

timedurationtickertimer

第34课:时间与定时器

学习目标

通过本课学习,你将能够:

  1. 理解Go语言中时间的表示与格式化
  2. 使用time.Duration进行时间间隔计算
  3. 掌握time.Timer实现单次定时任务
  4. 运用time.Ticker实现周期性定时任务
  5. 了解如何在并发编程中安全地使用定时器

核心概念

时间的基本表示

在Go中,time.Time类型表示一个具体的时间点。获取当前时间非常简单:

now := time.Now() // 获取当前本地时间

时间段(Duration)

time.Duration表示两个时间点之间的时间间隔,单位为纳秒。我们常用time.Secondtime.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:心跳检测(较难)

创建一个模拟心跳检测的程序:

  1. 有一个"服务器"每500毫秒发送一次心跳信号
  2. 有一个"客户端"监听心跳,如果超过2秒没收到心跳,打印"服务器连接丢失"
  3. 运行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时间格式

小结

关键要点回顾

  1. 时间表示:使用time.Now()获取当前时间,Format()进行格式化输出
  2. 时间段time.Duration表示时间间隔,支持加减乘除运算
  3. Timer:单次定时器,通过NewTimer()创建,监听.C通道获取触发事件
  4. Ticker:周期打点器,通过NewTicker()创建,需要及时调用Stop()避免泄漏
  5. 超时控制:结合select语句和Timer/Ticker实现超时控制和周期任务

使用技巧

  • 定时器通道的读取是非阻塞的(如果还没到时间)
  • 在并发程序中,Timer和Ticker的通道可以安全地在多个goroutine间传递
  • 使用time.Sleep()进行简单等待,但定时器更适合复杂的并发场景

下一步学习

下一课我们将学习如何获取和处理命令行参数与环境变量,这是编写实用CLI工具的重要技能。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「命令行参数与环境变量」 以巩固所学知识。