当前位置: 首页 > news >正文

16 - Go 协程(goroutine):从基础到实战

文章目录

  • 🚀 16 - Go 协程(goroutine):从基础到实战
  • 什么是 goroutine?
    • 🚀 第一个 goroutine
  • goroutine 执行机制
    • 🔥 关键模型:GMP 模型
    • 🧠 调度流程(简化版)
    • 🚀 为什么 goroutine 很轻?
  • goroutine + channel(核心组合)
    • 📦 channel 基础
    • 示例:goroutine 通信
    • 🔁 带缓冲 channel
  • goroutine 实战场景
    • 🧪 并发任务处理
    • 🧪 使用 WaitGroup 控制并发
    • 🧪 并发安全(Mutex)
  • goroutine 常见坑(必会)
    • ❌ 主 goroutine 提前退出
    • ❌ 闭包变量问题(经典面试题)
    • ❌ goroutine 泄漏
  • goroutine 调度细节(进阶)
    • ⏱ 抢占式调度(Go 1.14+)
    • 🔄 调度时机
  • 性能优化建议
    • 🚀 控制 goroutine 数量
    • 🚀 使用 sync.Pool 复用对象
    • 🚀 合理使用 channel
  • 总结
    • 🎯 goroutine 核心要点
    • 🎯 并发三件套
    • 🎯 一句话总结
  • 📌 面试高频问题

🚀 16 - Go 协程(goroutine):从基础到实战

Go 的并发之所以强大,不是因为它快,而是因为它"简单且优雅"。

在 Go 语言中,并发编程的核心就是goroutine。它让你用极低的成本实现高并发,是 Go 被称为“云原生语言”的关键原因之一。


什么是 goroutine?

goroutine 是 Go 语言中的轻量级线程(用户态线程)

👉 特点:

  • 占用内存极小(初始 ~2KB)
  • 创建成本极低
  • 由 Go runtime 调度(而不是操作系统)
  • 可以轻松创建成千上万个

🚀 第一个 goroutine

packagemainimport("fmt""time")// 定义一个普通函数funchello(){fmt.Println("Hello, world!")// 打印一句话}funcmain(){gohello()// 使用 go 关键字启动一个 goroutine(协程)// 此时 hello() 会在一个新的协程中异步执行// main 函数不会等待它执行完time.Sleep(time.Second)// 让主 goroutine 休眠 1 秒// 作用:防止 main 提前退出// 如果没有这行代码,程序可能在 hello() 执行前就结束了}

👉 注意:

👉1. goroutine 是异步执行的

  • go hello()不会阻塞
  • main 会继续往下执行

👉2. main 退出 = 所有 goroutine 结束

  • 这是很多新手最容易踩的坑

goroutine 执行机制

🔥 关键模型:GMP 模型

Go 的调度核心是:

名称含义
GGoroutine
M线程(Machine)
PProcessor(调度器)

👉 关系:

G(任务) → P(队列) → M(执行)

🧠 调度流程(简化版)

  1. goroutine(G)加入队列
  2. P 负责调度 G
  3. M(线程)执行 G
  4. 遇到阻塞 → 切换其他 G

🚀 为什么 goroutine 很轻?

相比传统线程:

对比项线程goroutine
创建成本极低
内存MB级KB级
调度OSGo runtime
切换

goroutine + channel(核心组合)

Go 并发哲学:

不要通过共享内存来通信,而要通过通信来共享内存


📦 channel 基础

ch:=make(chanint)

示例:goroutine 通信

packagemainimport"fmt"// 定义一个 worker 函数,接收一个 int 类型的 channelfuncworker(chchanint){ch<-100// 向 channel 发送数据 100// 如果没有接收者,这里会阻塞(很关键)}funcmain(){ch:=make(chanint)// 创建一个无缓冲 channel(同步 channel)// 特点:发送和接收必须同时准备好,否则会阻塞goworker(ch)// 启动 goroutine 执行 worker// worker 会尝试向 channel 发送数据v:=<-ch// 从 channel 接收数据// 如果没有数据,这里会阻塞,直到有数据写入fmt.Println(v)// 输出接收到的值:100}

🔁 带缓冲 channel

packagemainimport"fmt"// 定义 worker 函数,参数是一个 int 类型的 channelfuncworker(chchanint){ch<-100// 向 channel 发送数据 100// 因为是带缓冲 channel,所以只要 buffer 没满就不会阻塞}funcmain(){ch:=make(chanint,2)// 创建一个带缓冲的 channel,容量为 2// 表示最多可以暂存 2 个 intch<-1// 第一次发送:放入 buffer[0]ch<-2// 第二次发送:放入 buffer[1]// 此时 buffer 已满(2/2)fmt.Println(<-ch)// 从 channel 取出一个值(1)// buffer 腾出一个位置fmt.Println(<-ch)// 再取出一个值(2)// 此时 buffer 为空goworker(ch)// 启动 goroutine 执行 worker// 因为 buffer 已经空出空间,所以可以正常写入 100fmt.Println(<-ch)// 从 channel 取出一个值(100)fmt.Println("main function")// 主函数继续执行,不会等待 worker}

输出:

1 2 100 main function

实际运行逻辑是:

创建 buffer = 2 的 channel
写入 1、2(buffer 满)
读取 1、2(buffer 清空)
启动 goroutine 写入 100
main 继续执行,读取 100

👉 特点:

  • 不会立即阻塞
  • 类似队列

goroutine 实战场景


🧪 并发任务处理

packagemainimport("fmt""time")// 定义一个任务函数,模拟耗时操作functask(idint){fmt.Println("start",id)// 打印任务开始time.Sleep(time.Second)// 模拟耗时 1 秒的业务逻辑(比如 IO / 网络 / DB)fmt.Println("end",id)// 打印任务结束}funcmain(){fori:=0;i<10;i++{gotask(i)// 启动 10 个 goroutine 并发执行 task// 每个 goroutine 处理一个 id}time.Sleep(2*time.Second)// 主 goroutine 休眠 2 秒// 作用:防止 main 函数提前退出// 否则子 goroutine 还没执行完程序就结束了}

输出:

start9start6start4start5start8start0start1start2start7start3end9end6end5end4end0end8end3end1end2end7

👉 输出是“交错的”
👉 重点:

goroutine 调度是抢占式 + 不可控顺序

  • 10 个 goroutine 同时进入调度队列
  • Go runtime 自动调度执行
  • 执行顺序 完全不确定

🧪 使用 WaitGroup 控制并发

packagemainimport("fmt""sync")// 定义一个任务函数,接收 id 和 WaitGroup 指针functask(idint,wg*sync.WaitGroup){deferwg.Done()// defer 保证函数结束时一定调用 Done()// 表示该 goroutine 执行完成,计数器 -1fmt.Println("task:",id)// 模拟任务执行}funcmain(){varwg sync.WaitGroup// 创建 WaitGroup,用于控制 goroutine 同步fori:=0;i<10;i++{wg.Add(1)// 每启动一个 goroutine,计数器 +1// 表示“还有一个任务未完成”gotask(i,&wg)// 启动 goroutine 执行任务// 注意:传指针,否则会拷贝 wg(错误写法)}wg.Wait()// 阻塞主 goroutine// 直到 wg 计数器变为 0(所有任务完成)}

输出:顺序不一的

task:9task:0task:1task:2task:3task:4task:5task:6task:7task:8

👉 推荐:生产环境必须用 WaitGroup,而不是 sleep

WaitGroup 是 Go 中用于“等待一组 goroutine 完成”的标准同步工具,本质是计数器控制并发生命周期。


🧪 并发安全(Mutex)

packagemainimport("fmt""sync")// 全局变量:共享资源(多个 goroutine 会同时访问)varcountint// 定义互斥锁,用于保护共享变量 countvarmu sync.Mutex// 定义任务函数,接收 WaitGroup 指针funcadd(wg*sync.WaitGroup){deferwg.Done()// goroutine 执行完成后通知 WaitGroup -1mu.Lock()// 加锁:同一时刻只允许一个 goroutine 进入临界区count++// 临界区:对共享变量进行修改(非原子操作)mu.Unlock()// 解锁:允许其他 goroutine 进入临界区}funcmain(){varwg sync.WaitGroup// 用于等待所有 goroutine 执行完成fori:=0;i<1000;i++{wg.Add(1)// 每启动一个 goroutine,计数 +1goadd(&wg)// 启动 goroutine 执行加法操作}wg.Wait()// 阻塞主 goroutine,等待所有任务完成fmt.Println(count)// 输出最终结果:1000}

👉 Go 设计哲学:

不要通过共享内存通信,而要通过通信共享内存

Mutex 的作用是保证共享资源在并发访问时的“互斥性”,从而避免数据竞争,保证程序结果正确。


goroutine 常见坑(必会)


❌ 主 goroutine 提前退出

gofunc(){fmt.Println("hello")}()

👉 可能不会执行!

✔ 解决:

  • WaitGroup
  • channel
  • 阻塞 main

❌ 闭包变量问题(经典面试题)

fori:=0;i<3;i++{gofunc(){fmt.Println(i)}()}

👉 可能输出:

3 3 3

✔ 正确写法:

fori:=0;i<3;i++{gofunc(iint){fmt.Println(i)}(i)}

❌ goroutine 泄漏

funcworker(chchanint){<-ch// 永远等不到}

👉 没有关闭 channel → goroutine 永久阻塞

正确写法:

funcworker(chchanint){forv:=rangech{fmt.Println(v)}}

主函数:

ch:=make(chanint)goworker(ch)ch<-1ch<-2close(ch)// 👈 关键:关闭 channel

goroutine 调度细节(进阶)


⏱ 抢占式调度(Go 1.14+)

以前:

  • 协程不会主动让出 CPU

现在:

  • Go runtime 会强制抢占

👉 优势:

  • 防止某个 goroutine 长时间占用 CPU

🔄 调度时机

goroutine 切换发生在:

  • channel 阻塞
  • IO 阻塞
  • 系统调用
  • runtime 主动调度

性能优化建议


🚀 控制 goroutine 数量

❌ 错误:

for{gotask()}

✔ 正确(使用 worker pool):

jobs:=make(chanint,100)forw:=0;w<5;w++{goworker(jobs)}

🚀 使用 sync.Pool 复用对象

减少 GC 压力(高并发场景)


🚀 合理使用 channel

  • 不要滥用
  • 简单场景用锁更高效

总结


🎯 goroutine 核心要点

  • go关键字开启协程
  • 本质是用户态线程
  • 由 GMP 模型调度
  • 与 channel 配合使用最优雅

🎯 并发三件套

  • goroutine
  • channel
  • sync(WaitGroup / Mutex)

🎯 一句话总结

goroutine 让并发变简单,但并发本身并不简单。


📌 面试高频问题

  1. goroutine 和线程区别?
    答:轻量级线程,由 Go runtime 管理。
  2. GMP 模型是什么?
    答:Go 运行时调度模型,包含 G(goroutine)、M(线程)和 P(处理器)。
  3. channel 是怎么实现的?
    答:基于管道通信,底层实现依赖于 goroutine。
  4. 如何避免 goroutine 泄漏?
    答:确保所有 goroutine 执行完毕,或使用 context 控制。
  5. select 的作用?
    答:多路复用,用于等待多个 channel 操作。

http://www.jsqmd.com/news/660696/

相关文章:

  • 告别卡顿!在Auto.js中用好多线程Threads,让你的自动化脚本飞起来
  • 用Python和C++搞定算法竞赛中的同余问题:从模运算到CRT实战代码
  • 中兴光猫工厂模式解锁实践:zteOnu工具深度解析与技术实现
  • 深度解析R3nzSkin内存换肤技术:实现游戏内容实时渲染的完整方案
  • OBS StreamFX插件实战教程:从零打造电影级直播画面
  • 3个核心痛点:UABEA如何帮你彻底解决Unity资源管理难题
  • 如何轻松提取抖音音频?这款免费工具让你效率提升10倍!
  • 保姆级教程:手把手教你用SIG官网完成蓝牙BQB列名(附Component QDID组合实战)
  • OWL ADVENTURE在网络安全中的应用:恶意图像与钓鱼网站视觉检测
  • 如何在3分钟内完成革命性远程桌面连接?BilldDesk Pro突破性解决方案揭秘
  • 别再硬扛多项式了!用Python的curve_fit搞定高斯拟合,实测物理实验数据处理
  • 发现你的跨平台文本编辑新伙伴:Notepad-- 如何让代码编写更高效
  • JPEXS免费Flash反编译器:5分钟掌握终极SWF资源提取与代码恢复技巧
  • 生物信息学新手村任务:5分钟上手,用Grabseqs一站式下载并转换SRA为Fastq
  • Java 面试:微服务与云原生技术的深度探讨
  • 从编译错误到精准选型:GD32F10x系列宏定义冲突的排查与解决指南
  • 基于Matlab的电磁波动态仿真:从正入射到通用函数封装
  • DeepSeek-R1-Distill-Qwen-1.5B场景应用:教育辅助+编程助手实战案例
  • PMP认证备考全攻略:费用、周期与机构选择常见问题解答
  • 终极解决方案:如何在Mac上让外接鼠标获得触控板般的丝滑滚动体验
  • IP反欺诈查询实战:跨境从业者如何识别虚假IP与恶意流量
  • 顺企网商品详情页前端性能优化实战
  • 终极指南:使用开源工具解决NVIDIA显卡显示器色彩失真问题
  • tao-8k在中小企业知识管理中的应用:基于Xinference的轻量RAG实践
  • Cursor Free VIP技术深度解析:如何实现跨平台AI编辑器试用限制绕过
  • Simple Clock:为什么这款开源时钟应用能成为你的高效时间管理助手?
  • mmdetection模型测试与可视化全攻略:用一条命令生成带预测框的结果图(show-dir参数详解)
  • 别再只盯着LSTM了!用PyTorch从零搭建TCN时间卷积网络,搞定时序预测任务
  • 如何在5分钟内将Word文档完美转换为LaTeX:docx2tex完整指南
  • 项目仪表板:多维度指标的可视化与报告