学习目标
- 理解 goroutine 的本质与调度模型
- 区分并发(concurrency)与并行(parallelism)
- 掌握 goroutine 的生命周期与常见误区
- 建立“goroutine 很轻,但不是免费的”工程认知
一、什么是 goroutine
goroutine 是 Go 提供的用户态并发执行单元,由 Go runtime 负责调度。
创建 goroutine:
go func() {
fmt.Println("hello goroutine")
}()与操作系统线程相比,goroutine 的典型特征:
- 创建成本低
- 初始栈小(会按需增长)
- 调度由 Go runtime 完成(不是直接由 OS 调度 goroutine)
工程结论:Go 可以轻松创建大量 goroutine,但滥用依然会导致资源耗尽与不可控并发。
二、并发(Concurrency) vs 并行(Parallelism)
并发:多个任务同时存在(结构上同时推进),不要求同一时刻都在执行。
并行:多个任务同一时刻真正同时执行,依赖多核 CPU 与运行时配置。
理解例子:
- 单核 CPU 上,多个 goroutine 轮流运行:并发
- 多核 CPU 上,多个 goroutine 同时运行:并行
补充:是否并行与 GOMAXPROCS(可用 P 的数量)相关。默认通常等于 CPU 核心数,但工程上不建议随意修改,除非你明确知道原因。
三、Go 调度模型(G-P-M)
Go runtime 使用 G-P-M 模型调度 goroutine(理解到“角色职责”层面即可)。
- G(Goroutine):要执行的任务实体(函数、栈、状态等)
- P(Processor):逻辑处理器,承载运行 goroutine 的上下文(队列、调度资源)
- M(Machine):操作系统线程,真正执行指令的载体
一句话概括:
- G 是任务
- P 是调度资源与队列
- M 是 OS 线程执行者
工程结论:你创建的是 G(goroutine),真正执行的是 M(线程),P 决定可并行的程度。
四、goroutine 的生命周期与“主 goroutine 退出”问题
goroutine 的生命周期与其所属进程绑定:主 goroutine 退出,进程结束,其它 goroutine 会被直接终止。
常见错误示例:
func main() {
go func() {
fmt.Println("work")
}()
// main 结束太快,子 goroutine 可能来不及执行
}可观察修复(仅用于演示,不是工程推荐手段):
func main() {
go func() {
fmt.Println("work")
}()
time.Sleep(100 * time.Millisecond) // 仅演示:让 goroutine 有机会运行
}工程推荐的正确方式:使用同步原语(例如 WaitGroup)。
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("work")
}()
wg.Wait() // 等待 goroutine 完成
}五、for + goroutine 的闭包捕获陷阱(必须掌握)
错误示例(经典坑):
func main() {
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Second)
}可能输出(不稳定,但常见):
3
3
3原因:
- 闭包捕获的是同一个变量
i(地址/引用),不是每次循环的值 - goroutine 执行时循环可能已经结束
正确写法 1:通过参数传值(推荐)
for i := 0; i < 3; i++ {
go func(v int) {
fmt.Println(v)
}(i)
}正确写法 2:重新声明变量(可用但不如传参直观)
for i := 0; i < 3; i++ {
i := i
go func() {
fmt.Println(i)
}()
}六、goroutine “很轻,但不是免费的”
goroutine 不是免费资源,常见成本来源:
- 内存:每个 goroutine 都有栈与运行期结构体(数量大了就显著)
- 调度:大量 goroutine 会增加调度与上下文切换开销
- 阻塞:I/O、锁、channel 阻塞会导致堆积
- 泄漏:goroutine 无法结束(等待永远不会发生的事件)
工程结论:你要控制并发规模,而不是“想开多少 goroutine 就开多少”。
七、goroutine 泄漏(Goroutine Leak)概念与例子
goroutine 泄漏指 goroutine 长期阻塞,无法退出,随时间积累导致资源耗尽。
示例:向无人接收的 channel 发送导致永远阻塞
func leak() {
ch := make(chan int)
go func() {
ch <- 1 // 没有接收者,会一直阻塞,goroutine 无法退出
}()
}修复思路:
- 确保有接收者
- 使用带缓冲 channel
- 使用 context 做取消与超时(后续 Day 15 会系统讲)
八、goroutine 与 panic(复习 Day 10 的关键点)
goroutine 中 panic 若不 recover,会导致整个进程崩溃。
推荐在 goroutine 入口加 recover 兜底(边界层):
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("goroutine panic:", r)
}
}()
// goroutine logic
}()工程建议:recover 只做兜底 + 记录,不能把 panic 当业务流程。
预告:Day 12|channel 基础(通信与同步原语)
下一天你将系统掌握:
- 无缓冲 / 有缓冲 channel 的语义差异
- 发送/接收的阻塞行为
- close 的规则与常见误用
- 如何用 channel 建模生产者/消费者