Files
cs-note/hzh/GolangStar/Go语言进阶/Goroutine.md
T

195 lines
5.5 KiB
Markdown
Raw Normal View History

2026-06-07 11:08:10 +08:00
---
2026-06-07 12:14:39 +08:00
tags: [go, golang, Goroutine, 并发, CSP]
create time: 2026-06-07 14:35
2026-06-07 11:08:10 +08:00
---
# Goroutine
2026-06-07 12:14:39 +08:00
## 概述
Goroutine 是 Go 语言对协程的原生支持,以极低的启动成本(约 2KB 栈)和灵活的栈扩容机制,让开发者可以轻易创建成千上万的并发任务。本文讲解 goroutine 的基本用法、生命周期管理以及最佳实践。
## 正文
### 为什么需要 Goroutine?
> [!question] 💭 思考
> 假设有 1000 个独立的 HTTP 请求要同时发出,用传统线程模型你会怎么做?
在 C++/Java 等传统语言中,线程栈大小通常为 2MB,且创建/销毁涉及系统调用,开销较大。因此这些语言通常使用线程池来复用线程。
Go 的 Goroutine 则完全不同:
| 对比项 | OS 线程 | Go Goroutine |
|--------|---------|-------------|
| 初始栈大小 | ~2MB | ~2KB |
| 最大栈大小 | 受限于系统内存 | 可达 1GB(动态扩容) |
| 创建方式 | 系统调用 | 用户态调度 |
| 典型并发量 | 数百 | 数十万 |
```mermaid
flowchart TD
A["main goroutine"] -->|"go func()"| B["goroutine 1"]
A -->|"go func()"| C["goroutine 2"]
A -->|"go func()"| D["goroutine n..."]
subgraph Runtime["Go Runtime GMP 调度器"]
G["Goroutine 队列"]
M["M: OS Thread"]
P["P: Processor"]
G --> M
P --> M
end
B --> Runtime
C --> Runtime
D --> Runtime
2026-06-07 11:08:10 +08:00
```
2026-06-07 12:14:39 +08:00
> [!info] ℹ️ 补充
> Goroutine 由 Go 运行时通过 **GMP 调度模型**管理——G(goroutine)、M(OS thread)、P(processor)。你只需 `go` 关键字即可交给运行时调度,无需关心底层细节。详见 [[hzh/GolangStar/Go语言原理/gmp调度原理]]。
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
### 基本用法
开启一个 goroutine 只需要在函数调用前加 `go` 关键字:
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
```go
func task(name string) {
fmt.Printf("%s running\n", name)
2026-06-07 11:08:10 +08:00
}
func main() {
2026-06-07 12:14:39 +08:00
go task("worker") // 异步执行,不阻塞主流程
fmt.Println("main continues immediately")
2026-06-07 11:08:10 +08:00
}
```
2026-06-07 12:14:39 +08:00
> [!warning] ⚠️ 关键陷阱:主协程退出 = 全部终止
> 当 `main` 函数返回时,程序立即结束——所有其他 goroutine 无论是否执行完都会被强制终止。这与 Java/C++ 中后台线程继续运行的行为不同。
2026-06-07 11:08:10 +08:00
```go
package main
2026-06-07 12:14:39 +08:00
import "fmt"
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
func myTask() {
fmt.Println("子协程输出")
2026-06-07 11:08:10 +08:00
}
func main() {
2026-06-07 12:14:39 +08:00
go myTask()
fmt.Println("end!!!")
// ❌ 这里如果没有等待,myTask 很可能来不及执行
2026-06-07 11:08:10 +08:00
}
2026-06-07 12:14:39 +08:00
// 可能只输出: end!!!
2026-06-07 11:08:10 +08:00
```
2026-06-07 12:14:39 +08:00
上面的代码大概率只打印 `"end!!!"`,因为主协程执行完就退出了,子协程还没来得及运行。**正确做法是使用同步原语等待**:
2026-06-07 11:08:10 +08:00
```go
package main
import (
2026-06-07 12:14:39 +08:00
"fmt"
"sync"
2026-06-07 11:08:10 +08:00
)
2026-06-07 12:14:39 +08:00
func myTask(name string, wg *sync.WaitGroup) {
defer wg.Done() // 标记本 goroutine 完成
fmt.Printf("%s running\n", name)
2026-06-07 11:08:10 +08:00
}
func main() {
2026-06-07 12:14:39 +08:00
var wg sync.WaitGroup
wg.Add(2)
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
go myTask("goroutine1", &wg)
go myTask("goroutine2", &wg)
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
wg.Wait() // 阻塞直到计数器归零
fmt.Println("all done")
2026-06-07 11:08:10 +08:00
}
```
2026-06-07 12:14:39 +08:00
> [!tip] 💡 最佳实践
> - 始终使用 `sync.WaitGroup` / channel / context 来管理 goroutine 生命周期,而非 `time.Sleep`
> - `defer wg.Done()` 放在 goroutine 入口第一行,确保即使 panic 也能正确递减计数器
### 竞态条件与数据竞争
> [!question] 💭 思考
> 多个 goroutine 同时读写同一个变量会发生什么?就像两个人同时修改同一张纸上的数字。
当两个或多个 goroutine 同时访问同一块内存,且至少有一个是写操作时,就会发生**数据竞争(Data Race)**。这是 Go 中最常见的并发 bug 之一。
```go
var counter int
func increment() {
counter++ // ❌ 危险!多个 goroutine 同时读写 counter
}
func main() {
for i := 0; i < 1000; i++ {
go increment()
}
time.Sleep(time.Second)
fmt.Println(counter) // 结果不确定!
}
2026-06-07 11:08:10 +08:00
```
2026-06-07 12:14:39 +08:00
检测数据竞争:Go 提供了内置工具——编译时加上 `-race` 标志即可启用 race detector:
```bash
go run -race main.go
go test -race ./...
2026-06-07 11:08:10 +08:00
```
2026-06-07 12:14:39 +08:00
解决方式见 [[hzh/GolangStar/Go语言进阶/Sync]](Mutex / Atomic / Channel)。
### Goroutine Leak
> [!warning] ⚠️ 隐形杀手:Goroutine Leak
> 如果一个 goroutine 永远无法结束(比如等待永远不会到来的 channel 消息),它就会一直驻留在内存中——这就是 goroutine leak。它比内存泄漏更难发现,因为 GC 不会回收仍在活跃的 goroutine。
常见泄漏场景:
```go
// 场景1:channel 未关闭,接收方永久阻塞
func reader(ch <-chan int) {
v := <-ch // 如果 ch 永远不会关闭或写入,这里永久阻塞
}
// 场景2:select 中没有 default,且所有 case 都无响应
func selectLoop() {
ch := make(chan int)
select {
case v := <-ch: // 无 goroutine 往 ch 写数据
fmt.Println(v)
} // 死锁
}
// 场景3:超时未设置
func fetchWithTimeout() {
result := make(chan string, 1)
go func() {
result <- doSlowWork() // 可能永远不返回
}()
// ❌ 没有超时机制,goroutine 可能永远挂起
<-result
}
```
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
> [!tip] 💡 避免泄漏的 checklist
> - 每个发送端应在合适时机调用 `close(ch)`
> - 使用 `context.WithTimeout` 为所有外部调用设置超时
> - 在 select 中使用 `default` 实现非阻塞检查
> - 使用 `goleak` 库在测试中检测泄漏:`go.uber.org/goleak`
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
## 关联笔记
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
- [[hzh/GolangStar/Go语言进阶/Channel]]
- [[hzh/GolangStar/Go语言进阶/Sync]]
- [[hzh/GolangStar/Go语言进阶/Context]]
- [[hzh/GolangStar/Go语言进阶/并发概述]]