Go语言并发定时任务之从Sleep到Context的8种写法全解析

 更新时间:2025年08月15日 08:19:39   作者:mCell  
这篇文章主要为大家详细介绍了Go语言并发定时任务之从Sleep到Context的8种写法的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下

本文从 0 开始,带你用一个超简单的任务(每秒打印一个数字)学会:

  • Go time 包的几种定时 API
  • goroutineWaitGroupchannelselectcontext 的用法
  • 哪些并发写法是坑,哪些是生产推荐
  • 完整可运行的 main.go

背景

任务:每秒打印一个数字,连续 10 次。

隐含要求

  • 间隔稳定(大约 1 秒)
  • 顺序正确(1 → 10)
  • 最好可以提前取消

为什么这个任务重要?

这个简单任务涵盖了并发编程的核心要素:定时控制顺序保证生命周期管理。掌握它,你就掌握了 Go 并发的基础!

1. 基础写法:time.Sleep

func UseTimeSleep() {
    for i := 1; i <= 10; i++ {
        // 暂停当前goroutine的执行1秒钟
        // time.Second是预定义的Duration常量,等于1,000,000,000纳秒
        time.Sleep(time.Second)

        // 打印当前数字
        fmt.Println(i)
    }
}

关键 API 详解

  • time.Sleep(d Duration):暂停当前 goroutine 的执行至少 d 时长
  • time.Second:预定义的 Duration 常量,表示 1 秒(1e9 纳秒)
  • fmt.Println:标准输出函数,线程安全

优点

  • 顺序正确
  • 最简单易懂

缺点

  • 无法中途取消
  • 实际间隔 = 1 秒 + 打印耗时,会有累积误差
  • 阻塞当前 goroutine,无法同时执行其他任务

适用场景:简单脚本、不需要取消功能的短期任务

2.time.After:一次定时一次信号

func UseTimeAfter() {
    for i := 1; i <= 10; i++ {
        // time.After返回一个单向接收通道(<-chan Time)
        // 该通道会在指定时间后发送一个时间值
        timerChannel := time.After(time.Second)

        // <-操作符会阻塞,直到通道发送值
        <-timerChannel

        fmt.Println(i)
    }
}

关键点解析

  • time.After(d Duration) <-chan Time:返回一个只接收通道
  • 通道操作<-ch阻塞操作,直到有数据可读
  • 每次循环都创建新 Timer 对象,有 GC 压力

潜在问题

  • 频繁创建 Timer 对象,可能引起 GC 压力
  • 无法复用定时器
  • 同样无法取消

适用场景:单次超时控制,如网络请求超时

3.time.NewTicker:复用定时器

func UseTimeTicker() {
    // 创建Ticker对象,每1秒向C通道发送当前时间
    ticker := time.NewTicker(time.Second)

    // defer确保函数退出时停止Ticker,释放资源
    defer ticker.Stop()

    for i := 1; i <= 10; i++ {
        // 从Ticker的C通道读取值(阻塞直到时间到)
        <-ticker.C

        fmt.Println(i)
    }
}

Ticker 对象解析

  • time.NewTicker(d Duration) *Ticker:创建周期性定时器
  • ticker.C <-chan Time:定时触发的通道
  • ticker.Stop():停止定时器,必须调用否则资源泄漏

优点

  • 单一定时器复用,高效
  • 间隔精确

注意事项

  • 忘记 Stop()会导致 goroutine 泄漏
  • 使用后应立即 defer Stop()

适用场景:周期性任务,如定时数据采集

4. 并发误区

4.1channel并发版(顺序混乱)

func UseChannel() {
    // 创建无缓冲通道,发送和接收会同步阻塞
    ch := make(chan int)

    for i := 1; i <= 10; i++ {
        // 启动goroutine(轻量级线程)
        go func(num int) {
            // 每个goroutine等待不同时间
            time.Sleep(time.Second * time.Duration(num))

            // 向通道发送数字
            ch <- num
        }(i) // 注意:必须传入i的副本,避免闭包捕获问题
    }

    for i := 1; i <= 10; i++ {
        // 从通道接收值(阻塞直到有数据)
        value := <-ch
        fmt.Println(value)
    }
}

执行顺序解析

问题分析

  • 10 个 goroutine 并发执行,完成顺序不确定
  • 通道接收顺序 = goroutine 完成顺序 ≠ 数字顺序
  • 间隔时间不固定(1 秒到 10 秒)

正确使用场景:独立任务并行处理,如批量图片处理

4.2WaitGroup并发版(顺序混乱)

func UseGoroutine() {
    // 创建WaitGroup用于等待所有goroutine完成
    var wg sync.WaitGroup

    for i := 1; i <= 10; i++ {
        // 增加等待计数
        wg.Add(1)

        go func(num int) {
            // 函数退出时减少计数
            defer wg.Done()

            time.Sleep(time.Second * time.Duration(num))
            fmt.Println(num)
        }(i)
    }

    // 阻塞直到所有goroutine完成
    wg.Wait()
}

WaitGroup 原理

  • Add(delta int):增加等待计数
  • Done():减少计数(等价于 Add(-1))
  • Wait():阻塞直到计数归零

问题分析

  • 输出顺序完全随机
  • 间隔时间不固定
  • 多个 goroutine 同时调用 fmt.Println 可能输出交错

适用场景:并行执行独立任务,不需要顺序保证

5. 正确的并发写法

5.1 单 goroutine + WaitGroup

func UseSingleGoroutine() {
    var wg sync.WaitGroup

    // 只需等待1个goroutine
    wg.Add(1)

    // 启动工作goroutine
    go func() {
        // 确保结束时通知WaitGroup
        defer wg.Done()

        for i := 1; i <= 10; i++ {
            fmt.Println(i)
            time.Sleep(time.Second)
        }
    }()

    // 主goroutine等待工作完成
    wg.Wait()
}

架构解析

主goroutine         工作goroutine
    │                    │
    │── wg.Add(1) ────>▶│
    │                    │
    │                    ├─ 执行循环
    │                    │   打印+等待
    │                    │
    │◀─── wg.Wait() ─────┤
    ▼                    ▼

优点

  • 顺序和间隔完全可控
  • 结构清晰
  • 为添加取消功能留出空间

适用场景:需要顺序执行的定时任务

5.2select+Ticker

func UseSelectAndTicker() {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for i := 1; i <= 10; i++ {
        // select监控多个通道操作
        select {
        // 当ticker.C有值时执行
        case <-ticker.C:
            fmt.Println(i)
        }
    }
}

select 关键字详解

  • 用于监听多个通道操作
  • 当任意 case 可执行时,随机选择一个执行
  • 无 default 时会阻塞
  • 常用于多路复用

进阶用法(添加退出通道):

quit := make(chan struct{})
// ...
select {
case <-ticker.C:
    fmt.Println(i)
case <-quit:
    fmt.Println("提前退出")
    return
}

适用场景:需要监控多个事件源的定时任务

5.3context+Ticker(生产推荐)

func UseContextWithTicker() {
    // 创建可取消的context
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 确保资源释放

    ticker := time.NewTicker(time.Second)
    defer ticker.Stop() // 确保停止ticker

    i := 1
    for {
        select {
        case <-ticker.C: // 定时触发
            fmt.Println(i)
            i++
            if i > 10 {
                return // 自然结束
            }

        case <-ctx.Done(): // 上下文取消
            fmt.Println("任务取消:", ctx.Err())
            return
        }
    }
}

context 包深度解析

核心优势

  • 取消传播:一次取消,所有监听组件都能收到通知
  • 超时控制:可添加 WithTimeout 自动取消
  • 资源安全:defer 确保资源释放
  • 标准统一:Go 标准库广泛使用

实际应用

// 带超时的context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// 在数据库查询中使用
err := db.QueryContext(ctx, "SELECT...")

适用场景:所有生产环境需要生命周期管理的并发任务

6. 方法对比

方案顺序间隔可取消资源回收复杂度适用场景
Sleep一般简单脚本
After单次超时
Ticker⭐⭐周期性任务
Channel 并发⭐⭐⭐并行独立任务(不要求顺序)
WaitGroup 并发⭐⭐并行独立任务(简单同步)
单 goroutine+WG⭐⭐顺序定时任务
select+Ticker⚠️*⭐⭐⭐多事件源监控
context+Ticker⭐⭐⭐⭐生产级定时任务(推荐)

*需自行实现取消通道

7. 完整可运行示例

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

// 所有实现函数(前面已详细解释)

func main() {
    fmt.Println("=== 基础: time.Sleep ===")
    UseTimeSleep()

    fmt.Println("\n=== 基础: time.After ===")
    UseTimeAfter()

    fmt.Println("\n=== 基础: time.Ticker ===")
    UseTimeTicker()

    fmt.Println("\n=== 误区: Channel并发 ===")
    UseChannel()

    fmt.Println("\n=== 误区: WaitGroup并发 ===")
    UseGoroutine()

    fmt.Println("\n=== 正确: 单goroutine+WaitGroup ===")
    UseSingleGoroutine()

    fmt.Println("\n=== 正确: select+Ticker ===")
    UseSelectAndTicker()

    fmt.Println("\n=== 生产推荐: context+Ticker ===")
    UseContextWithTicker()

    fmt.Println("\n=== 所有示例执行完成 ===")
}

运行方式

# 运行程序
go run main.go

# 输出示例:
=== 基础: time.Sleep ===
1
2
...
10

=== 误区: Channel并发 ===
1
3
2
... # 顺序随机

进阶学习建议

context 的更多用法

  • context.WithTimeout:自动超时取消
  • context.WithValue:传递请求范围数据
  • 上下文传递规范

错误处理模式

select {
case <-ctx.Done():
    return ctx.Err()
case err := <-errCh:
    return err
}

资源管理最佳实践

  • 总是 defer 关闭资源
  • 使用sync.Once确保单次初始化
  • 避免在循环中创建 goroutine

性能调优

  • 使用pprof分析 goroutine 泄漏
  • 合理设置 GOMAXPROCS
  • 避免过度并发

实际项目中的并发往往更复杂,但核心原理不变。掌握这些基础模式,你就能构建健壮的并发系统!

到此这篇关于Go语言并发定时任务之从Sleep到Context的8种写法全解析的文章就介绍到这了,更多相关Go语言并发定时任务内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go-Web框架中AOP方案的实现方式

    Go-Web框架中AOP方案的实现方式

    本文主要介绍了Go-Web框架中AOP方案的实现方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • 使用Go语言解决Scan空格结束输入问题

    使用Go语言解决Scan空格结束输入问题

    这篇文章主要为大家介绍了使用Go语言来解决Scan空格结束输入问题,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2021-11-11
  • Golang配置管理Viper的实现

    Golang配置管理Viper的实现

    Viper是Go语言配置管理库,支持JSON/YAML/TOML等多格式,可动态监听配置变更,本文就来介绍一下Golang配置管理Viper的实现,感兴趣的可以了解一下
    2025-07-07
  • 使用go语言实现Redis持久化的示例代码

    使用go语言实现Redis持久化的示例代码

    redis 是一个内存数据库,如果你把进程杀掉,那么里面存储的数据都会消失,那么这篇文章就是来解决 redis 持久化的问题,本文给大家介绍了使用go语言实现Redis持久化,需要的朋友可以参考下
    2024-07-07
  • Golang基于Vault实现敏感数据加解密

    Golang基于Vault实现敏感数据加解密

    数据加密是主要的数据安全防护技术之一,敏感数据应该加密存储在数据库中,降低泄露风险,本文将介绍一下利用Vault实现敏感数据加解密的方法,需要的可以参考一下
    2023-07-07
  • go中值传递和指针传递的使用

    go中值传递和指针传递的使用

    在Go语言中,使用&和*可以分别取得变量的地址和值,解引用未初始化或为nil的指针会引发空指针异常,正确的做法是先进行nil检查,此外,nil在Go中用于多种类型的空值表示,值传递和指针传递各有适用场景,通常小型数据结构优先考虑值传递以减少解引用开销
    2024-10-10
  • go语言中函数与方法介绍

    go语言中函数与方法介绍

    这篇文章介绍了go语言中的函数与方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • gorm FirstOrCreate和受影响的行数实例

    gorm FirstOrCreate和受影响的行数实例

    这篇文章主要介绍了gorm FirstOrCreate和受影响的行数实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 详解Go语言中的结构体的特性

    详解Go语言中的结构体的特性

    结构体是Go语言中重要且灵活的概念之一,本文旨在深入介绍Go语言中的结构体,揭示其重要性和灵活性,并向读者展示结构体支持的众多特性,需要的可以参考一下
    2023-06-06
  • Go源码字符串规范检查lint工具strchecker使用详解

    Go源码字符串规范检查lint工具strchecker使用详解

    这篇文章主要为大家介绍了Go源码字符串规范检查lint工具strchecker使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06

最新评论