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 的调度核心是:
| 名称 | 含义 |
|---|---|
| G | Goroutine |
| M | 线程(Machine) |
| P | Processor(调度器) |
👉 关系:
G(任务) → P(队列) → M(执行)🧠 调度流程(简化版)
- goroutine(G)加入队列
- P 负责调度 G
- M(线程)执行 G
- 遇到阻塞 → 切换其他 G
🚀 为什么 goroutine 很轻?
相比传统线程:
| 对比项 | 线程 | goroutine |
|---|---|---|
| 创建成本 | 高 | 极低 |
| 内存 | MB级 | KB级 |
| 调度 | OS | Go 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)// 👈 关键:关闭 channelgoroutine 调度细节(进阶)
⏱ 抢占式调度(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 让并发变简单,但并发本身并不简单。
📌 面试高频问题
- goroutine 和线程区别?
答:轻量级线程,由 Go runtime 管理。 - GMP 模型是什么?
答:Go 运行时调度模型,包含 G(goroutine)、M(线程)和 P(处理器)。 - channel 是怎么实现的?
答:基于管道通信,底层实现依赖于 goroutine。 - 如何避免 goroutine 泄漏?
答:确保所有 goroutine 执行完毕,或使用 context 控制。 - select 的作用?
答:多路复用,用于等待多个 channel 操作。
