Go goroutine
goroutine 是 Go 运行时调度的轻量并发任务:启动成本低,但不自动同步、不自动回收错误,生命周期必须由设计者显式管理。
[!info] 关联笔记
Go goroutine
这个概念为什么会出现
网络服务、批量 I/O、扇出请求时,如果每个任务都绑定一条操作系统线程,成本会迅速升高。Go 把“启动一个并发任务”做成语言级操作:
go doWork()
这解决了启动成本问题,却立刻带来新问题:
- 谁等待任务结束?
- 错误交给谁?
- 如何取消?
- 共享变量如何可见?
goroutine 不是“自动正确的并行”,而是必须配套生命周期设计的并发执行单元。
[!abstract] 一句话理解
go f()让f在独立的 goroutine 中执行;调用方立即继续。运行时负责多路复用到 OS 线程,但同步、退出与错误传播必须显式设计。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:下单后异步发“欢迎短信”
HTTP handler 写完订单后,不想让用户干等短信网关。
常见写法:go sendWelcomeSMS(userID) 丢到后台。
但有两个硬约束:
- main/请求函数如果立刻返回且进程退出,后台任务可能根本没跑完
- 必须能回答:谁等待结束?失败了谁知道?(本最小例先解决“等待结束”)
WaitGroup 就是“我启动了 N 个后台活,收工前等它们 Done”的计数器。
package main
import (
"fmt"
"sync"
)
// sendWelcomeSMS 模拟“给用户发欢迎短信”的慢 I/O。
// 业务上可能调第三方 SMS API;这里只打印证明任务跑到了。
func sendWelcomeSMS(userID int) {
fmt.Println("sms sent to user", userID)
}
func main() {
// 模拟:下单成功后要通知用户 1。
userID := 1
var wg sync.WaitGroup
// 预定:我要等 1 个后台任务。
wg.Add(1)
// go:在新的 goroutine 里跑,main 不会自动等它。
// 参数 userID 按值传入,避免闭包捕获外部变量的歧义
//(循环里启动多个 go 时尤其重要,见闭包笔记)。
go func(id int) {
// defer Done:无论 send 正常返回还是以后加了早期 return,
// 都会把 WaitGroup 计数减一,避免 main 永久卡住。
defer wg.Done()
sendWelcomeSMS(id)
}(userID)
// 业务上:进程/请求收尾前必须等通知发出(或交给更完整的 worker 池)。
// 没有 Wait,main 一返回整个程序结束,短信可能根本来不及发。
wg.Wait()
fmt.Println("checkout finished")
}
建议运行:
go run .
期望输出(两行顺序通常如下;严格交错时仍应两行都在):
sms sent to user 1
checkout finished
结合场景再看三个关注点
-
go只负责启动,不负责等待
没有WaitGroup/channel/errgroup,main返回会直接杀掉未完成任务。 -
参数按值传入更干净
go func(id int){...}(userID)把“给谁发短信”固定在启动瞬间。 -
defer wg.Done()绑在任务出口
对应“短信函数怎么返回,计数都要减”,避免少 Done 导致死等。
核心概念与准确模型
go 语句
go Expression
表达式通常是函数或方法调用。调用在新的 goroutine 中求值;当前 goroutine 不自动等待。
与 OS 线程的关系
| OS 线程 | goroutine | |
|---|---|---|
| 创建成本 | 相对高 | 相对低(实现细节,量级常远小于线程) |
| 调度 | 内核 | Go 运行时(G-M-P 模型,见进阶笔记) |
| 数量 | 受限于系统 | 可大量存在,但仍受内存与逻辑约束 |
| 阻塞 | 占用线程 | 运行时可能把线程用于其他 G(网络轮询等场景) |
语言保证的是并发语义与内存模型,而不是“一定比线程快 N 倍”。性能结论要测量。
生命周期责任
Wiki/Code Review 强调 goroutine 生命周期必须清晰:Goroutine Lifetimes
启动前先回答:
- 谁创建?
- 谁等待(join)?
- 谁取消?
- 可能阻塞在哪里(channel、锁、I/O)?
- 错误如何回到调用方?
flowchart LR
Main["main/请求处理"] -->|go| G["goroutine"]
G -->|结果/错误| Ch["channel 或共享结构"]
Main -->|cancel| Ctx["context"]
Ctx --> G
Main -->|Wait| WG["WaitGroup"]
没有强杀 API
Go 不提供“强制杀掉任意 goroutine”的安全通用 API。任务应:
- 自然返回,或
- 监听
context.Done()/ 取消 channel
runtime.Goexit 只结束当前 goroutine,不是跨任务取消框架。
错误不会自动传播
goroutine 内的 return err 只影响该函数。常见模式:
- 结果 channel:
chan result errgroup(golang.org/x/sync/errgroup)- 在文档化的同步点收集错误
panic 会在该 goroutine 内展开;若未 recover,默认使整个程序崩溃。不要依赖跨 goroutine 的 panic 传递。
设计动机
- 语言级并发
把启动任务写进语法,而不是库回调。 - CSP 风格主线
与 channel 搭配,鼓励“通信共享内存”。 - 工程默认值
降低线程心智,但通过显式同步把正确性成本留给设计。
边界情况与反直觉行为
1. main 结束即进程结束
go fmt.Println("maybe never")
// 无等待直接返回
2. 闭包捕获循环变量
Go 1.22 起 for 循环变量语义有调整;跨版本代码仍建议显式传参:
for i := 0; i < 3; i++ {
go func(i int) { fmt.Println(i) }(i)
}
3. 数据竞争
// 危险:无同步并发写
go func() { x++ }()
go func() { x++ }()
用 go test -race 检测(见 race detector)。
4. 泄漏
goroutine 卡在永远无发送方的接收、或未读满的发送上,会常驻到进程结束。
5. 并发 ≠ 并行
单核或 GOMAXPROCS=1 时仍是并发交错;并行需要多核与运行时调度允许。
常见误区
[!warning] 常见误区:把 goroutine 当线程 API 错误:按 pthread 思维创建/join/kill。
正确:任务 + channel/context/WaitGroup 协作。
[!warning] 常见误区:fire-and-forget 无边界 错误:请求路径随意
go且不限流。
正确:有超时、有取消、有并发上限(worker pool)。
[!warning] 常见误区:在 goroutine 里直接写请求的 ResponseWriter 错误:HTTP handler 返回后仍写响应。
正确:理解请求生命周期;需要异步则自行设计完成协议。
工程实践
- 先同步写对,再
go - 每个
go都有退出路径 - 用 context 贯穿取消(go-context)
- 限制并发度:信号量 channel 或 worker pool(go-worker-pool)
- 测试加
-race - 日志带请求 ID,否则并发堆栈难读
- 不要在库中偷偷启动不受控 goroutine;若必须启动,文档说明谁负责停
可验证实验
- 去掉
WaitGroup,观察输出是否丢失。 - 有意制造数据竞争并用
go test -race。 - 启动向无接收者的无缓冲 channel 发送的 goroutine,用
pprof/超时观察泄漏。 - 对比
GOMAXPROCS对 CPU 密集任务的影响(测量,不下绝对结论)。
本节总结
- 本质:轻量并发任务,由运行时调度。
- 关键规则:不自动等待、不自动传错、无强杀、共享必须同步。
- 最易错:泄漏、闭包、main 提前退出、无取消。
- 下一步:channel 作为通信与同步主线。
自测题
概念题
- 为什么说 goroutine 不是 OS 线程的别名?
- 启动 goroutine 前应回答哪几个生命周期问题?
- goroutine 内返回的 error 会怎样?
代码推理题
func main() {
ch := make(chan int)
go func() { ch <- 1 }()
// 无接收
}
程序行为可能是什么?
工程思考题
API handler 要并行请求 3 个下游。如何保证:任一失败可取消其余、总超时 2s、错误可返回客户端?
参考答案
展开
- 由 Go 运行时多路复用,语义与成本模型不同。
- 等待者、取消者、阻塞点、错误归属。
- 仅留在该调用栈,除非经 channel/结构体传出。
代码题:main 可能在发送完成前结束;或短暂阻塞后进程退出;属于未定义完整同步的坏例子。
工程:context+timeout,errgroup 或 channel 收集,下游请求绑定 ctx。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| Spec — Go statements | 规范 | go 语句 |
| CodeReviewComments — Goroutine lifetimes | 官方 Wiki | 生命周期审查标准 |
| Effective Go — Concurrency | 官方文档 | 并发风格 |
| Go Memory Model | 规范 | 可见性与同步 |
笔记元信息
- 建议文件名:
go-goroutines.md - 所属阶段:阶段四
- 本篇状态:已深化
- 建议下一篇:Go channel