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语言进阶/并发概述]]
|