# Go Goroutine


goroutine 是 Go 并发面试的核心，但这个系列已经拆成了好几篇：调度模型在 [Go GMP 模型](/posts/go-gmp/)，channel 的语义和内存模型在 [Go channel](/posts/go-channel/)，锁在 [Go Mutex](/posts/go-mutex/)，context 在 [Go Context](/posts/go-context/)，还有 [Go sync.Once](/posts/go-sync-once/)。这篇补上系列里没细写的部分：goroutine 和线程的区别、panic 跨 goroutine、race 检测、WaitGroup、泄漏排查，最后给一张考点地图。

## 1. goroutine 和线程的区别

goroutine 是 Go 运行时管理的轻量级协程，跑在操作系统线程之上，多个 goroutine 复用少量线程。官方 FAQ 的说法是：把协程多路复用到一组线程上，某个 goroutine 阻塞时，运行时自动把同一条线程上的其他 goroutine 挪到别的可运行线程上。

和线程比，差距在三个地方：

- **栈大小**。goroutine 初始栈只有 2KB（Go 1.4 起，`stackMin`），分配在堆上，不够了自动增长；线程栈通常以 MB 计（Linux 上默认 8MB），创建线程要预留一大块虚拟内存。
- **切换成本**。goroutine 切换在用户态完成，不陷入内核；线程切换要经过内核调度器，涉及上下文保存、内核态用户态切换。
- **数量级**。官方 FAQ 说一个地址空间里创建几十万个 goroutine 是可行的，线程到这个数量早就把系统资源耗光了。

「goroutine 很轻量」的底层原因就是：栈小、切换在用户态完成，运行时自己管理内存和调度。

注意别把「轻量」理解成没有成本。goroutine 本身有调度器开销，创建多了 GC 扫描的栈数量也涨；并发度不是越高越好，worker pool 限流是常见做法。

## 2. panic 和 recover

规则只有两条，但很多人答错：

- `recover` 只在 `defer` 函数里有效，直接调用 recover 返回 nil。
- 每个 goroutine 的 panic 只能由它自己的 recover 处理。某个 goroutine 里 panic 没被 recover，整个进程直接崩溃，其他 goroutine 里的 recover 救不了它。

经典错误示范：在 main 里 recover，子 goroutine 里 panic，以为能接住。崩了。正确写法是把 recover 放在 goroutine 自己的 defer 里：

```go
go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("goroutine panic: %v", r)
        }
    }()
    // 干活
}()
```

对外提供 goroutine 的库函数，标准做法是函数内部起 goroutine 并自带 recover，把 panic 转成 error 返回，别让调用方进程被拖垮。

另外区分 `panic` 和 `fatal error`：panic 能 recover，`sync: unlock of unlocked mutex` 这类 fatal error 是运行时直接崩溃，recover 接不住（详见 [Go Mutex](/posts/go-mutex/)）。

## 3. 数据竞争与 race 检测

多个 goroutine 同时读写同一变量就是数据竞争，后果是读到脏数据，甚至崩。Go 的检测手段是 race detector：

```bash
go build -race ./...
go test -race ./...
```

带 `-race` 编译出的二进制运行时检测到竞争会直接报错退出。面试时提到「写代码要开 race 检测」是加分项。

触发竞争的主要场景：goroutine 闭包捕获了外层变量（循环变量）、共享 map 并发读写、读写共享字段没加锁。预防靠锁和原子操作，选型见 [Go Mutex](/posts/go-mutex/)，这里只说单个数字的计数器场景用 `sync/atomic` 比锁轻量：

```go
var counter atomic.Int64
counter.Add(1)
```

## 4. WaitGroup

等待一组 goroutine 结束。坑点就一个：`Add` 必须在 goroutine 启动之前调用，而且要在主 goroutine 里调，别在子 goroutine 里 Add，否则 Wait 可能先跑起来，计数还没加上就返回了。`Done` 用 defer 保证执行：

```go
var wg sync.WaitGroup
for _, task := range tasks {
    wg.Add(1)
    go func(t Task) {
        defer wg.Done()
        t.Run()
    }(task)
}
wg.Wait()
```

注意循环变量要作为参数传进闭包，直接用外层变量会全部读到最后一个值（Go 1.22 之前）。

Go 1.25 起有 `wg.Go`，把 Add、启动 goroutine、Done 合并成一个调用，内部就是 `Add(1)` + `go func()` + defer `Done()`，上面的手写模式可以替换成：

```go
var wg sync.WaitGroup
for _, task := range tasks {
    wg.Go(func() { task.Run() })
}
wg.Wait()
```

省掉手动 Add/Done，也就绕开了计数配对错误的坑。一个细节：`Go` 的调用时机要求和 Add 一样，必须在 Wait 之前（或上一轮 Wait 返回之后），复用的规则没变。

## 5. goroutine 泄漏与排查

goroutine 启动后永远等不到结束条件，占着栈和内存不放，数量多了还会拖慢 GC。典型场景：

- 往 channel 发数据，但没有接收方，发送者永久阻塞。
- 从 channel 收数据，但没人再发送，也没 close。
- `select` 里等一个永远不会来的 case，又没有 default。
- `time.Ticker` 创建后没 `Stop`，定时器泄漏。
- 死循环 goroutine 没有退出条件。

排查手段：

- `runtime.NumGoroutine()` 打点看数量是否持续上涨。
- pprof 的 goroutine profile：程序里 import `net/http/pprof`，然后 `go tool pprof http://localhost:6060/debug/pprof/goroutine`，能看到每个 goroutine 的堆栈和数量，卡在哪个 channel、哪行代码一目了然。

让 goroutine 可取消，标准做法是 context 或 done channel，配合 select。context 的取消信号会沿着调用链传递，子 context 也一起取消；用 `context.WithTimeout` 还能做到超时自动取消（详见 [Go Context](/posts/go-context/)）。核心原则：每个 goroutine 都要想清楚它怎么结束，答不上来就是泄漏隐患。

## 6. 考点地图

| 考点                                      | 在哪篇                               |
| ----------------------------------------- | ------------------------------------ |
| goroutine 和线程的区别、轻量原因          | 本文                                 |
| GMP 调度、work stealing、handoff、抢占    | [Go GMP 模型](/posts/go-gmp/)        |
| channel 关闭语义、hchan、select、内存模型 | [Go channel](/posts/go-channel/)     |
| panic 跨 goroutine、recover 规则          | 本文                                 |
| 数据竞争、race 检测、WaitGroup            | 本文                                 |
| Mutex/RWMutex 底层实现、饥饿模式          | [Go Mutex](/posts/go-mutex/)         |
| context 取消、超时、传值                  | [Go Context](/posts/go-context/)     |
| sync.Once                                 | [Go sync.Once](/posts/go-sync-once/) |
| goroutine 泄漏场景与排查                  | 本文                                 |


---

> 作者: Nite  
> URL: https://www.nite07.com/zh-cn/posts/gp-goroutine/  

