编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

    • 入门教程

      • README
      • Go简史
      • 基础语法
      • 数据类型
      • 运算符
      • 复合类型
      • 流程语句
      • 函数
      • 指针与逃逸
      • 结构体与方法
      • 接口与多态
      • 错误处理
      • 并发goroutine
        • 目录介绍
        • 12.1 本章学习目标
        • 12.2 goroutine 起步:go f()
        • 12.3 GMP 模型速览(卷三详讲)
        • 12.4 runtime.GOMAXPROCS 与 P 数量
        • 12.5 sync.WaitGroup:等所有 goroutine 完成
        • 12.6 context 包:取消、超时、传值
          • 12.6.1 四种 Context
          • 12.6.2 context.WithCancelCause(Go 1.20+)
          • 12.6.3 取消传播链
        • 12.7 goroutine 泄漏的 5 种模式
          • 12.7.1 模式 1:channel 无人接收
          • 12.7.2 模式 2:channel 无人发送
          • 12.7.3 模式 3:没有设置超时
          • 12.7.4 模式 4:死锁(循环依赖)
          • 12.7.5 模式 5:select 永远走不到退出分支
        • 12.8 主 goroutine 退出 = 程序退出
        • 12.9 综合示例:并发抓取 N 个 URL(带超时)
        • 12.10 本章底层原理(简介)
        • 12.11 Go 新手陷阱 Top 5
        • 12.12 思考题
        • 12.13 推荐阅读
      • 通道channel
      • 同步sync包
      • IO和文件
      • 标准库与泛型
      • 工程化与模块
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • JavaScript入门

  • CodeX
  • Go入门到精通
  • 入门教程
杨充
2026-05-21
目录

并发goroutine

# 第 12 章 并发:goroutine 与 context

goroutine 是 Go 的"招牌"——一行 go f() 启动协程,配合 context 做取消传播。 关键词:go 关键字、GMP 速览、runtime.GOMAXPROCS、context.Context、取消传播、sync.WaitGroup


# 目录介绍

  • 12.1 本章学习目标
  • 12.2 goroutine 起步:go f()
  • 12.3 GMP 模型速览(卷三详讲)
  • 12.4 runtime.GOMAXPROCS 与 P 数量
  • 12.5 sync.WaitGroup:等所有 goroutine 完成
  • 12.6 context 包:取消、超时、传值
    • 12.6.1 四种 Context(Background / TODO / WithCancel / WithTimeout / WithDeadline / WithValue)
    • 12.6.2 context.WithCancelCause(Go 1.20+)
    • 12.6.3 取消传播链
  • 12.7 goroutine 泄漏的 5 种模式
    • 12.7.1 模式 1:channel 无人接收
    • 12.7.2 模式 2:channel 无人发送
    • 12.7.3 模式 3:没有设置超时
    • 12.7.4 模式 4:死锁(循环依赖)
    • 12.7.5 模式 5:select 永远走不到退出分支
  • 12.8 主 goroutine 退出 = 程序退出
  • 12.9 综合示例:并发抓取 N 个 URL(带超时)
  • 12.10 本章底层原理(简介)
  • 12.11 Go 新手陷阱 Top 5
  • 12.12 思考题
  • 12.13 推荐阅读

# 12.1 本章学习目标

学完本章你应当能够:

  • ✅ 能用 go f() 启动 goroutine,解释它和 OS 线程的区别
  • ✅ 能解释 GMP 三个角色的关系(G=任务、M=线程、P=调度器)
  • ✅ 能用 sync.WaitGroup 等待一组 goroutine 完成
  • ✅ 能用 context.WithTimeout / context.WithCancel 做超时和取消控制
  • ✅ 能识别 5 种常见的 goroutine 泄漏模式,并写出修复方案
  • ✅ 能写出"并发抓取 N 个 URL 并合并结果"的完整程序
  • ✅ 知道 GOMAXPROCS 的作用,以及何时需要调它

本章是 Go 并发的入口。学完本章你会写并发程序;第 13 章 channel 和卷三"专栏博客"会带你深入底层。


# 12.2 goroutine 起步:go f()

goroutine 是 Go 的轻量级"线程"——由 Go runtime 调度,而非操作系统。

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 3; i++ {
        fmt.Println(s)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go say("goroutine")   // ① 启动一个新 goroutine——非阻塞
    say("main")           // ② 主 goroutine 同步执行
}
// 输出(交错):
// main
// goroutine
// main
// goroutine
// main
// goroutine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

goroutine vs 线程:

goroutine OS 线程
创建成本 ~2KB 栈 + 极少的 runtime 开销 ~1MB 栈 + OS 系统调用
调度者 Go runtime(用户态) 操作系统内核
创建速度 ~μs 级 ~μs-ms 级
并发数上限 百万级 数千级
上下文切换 用户态,~几十 ns 内核态,~1-2 μs

go 关键字的行为:

  1. 把函数调用包装成一个 goroutine
  2. 放进当前 P 的本地运行队列(runq)
  3. 立即返回——不等待被调函数执行
func main() {
    go fmt.Println("Hello") // 启动了 goroutine——但主函数可能先退出
    // 主 goroutine 退出 → 整个程序退出 → goroutine 被杀
}
// 这行可能永远不打印
1
2
3
4
5

⚠️ 关键认知:go f() 不阻塞——这也是泄漏和竞态的根源。你必须用同步机制(WaitGroup / channel / context)来协调 goroutine 之间的执行顺序。


# 12.3 GMP 模型速览(卷三详讲)

Go 的调度器用 GMP 三个角色来描述并发模型:

┌───────────────────────────────────────────────────┐
│                    Go Runtime                     │
│                                                   │
│   ┌─────┐   ┌─────┐   ┌─────┐                    │
│   │  G  │   │  G  │   │  G  │   ← goroutine     │
│   │     │   │     │   │     │     (用户代码)       │
│   └──┬──┘   └──┬──┘   └──┬──┘                    │
│      │         │         │                        │
│      ▼         ▼         ▼                        │
│   ┌─────────────────────────┐                     │
│   │           P             │   ← Processor       │
│   │   (本地队列 + 调度上下文)  │     (调度器)         │
│   └───────────┬─────────────┘                     │
│               │                                    │
│               ▼                                    │
│   ┌─────────────────────────┐                     │
│   │           M             │   ← Machine         │
│   │      (OS 线程)          │     (执行者)         │
│   └─────────────────────────┘                     │
└───────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • G(Goroutine):你的 go f() 创建的就是一个 G。它包含栈、指令指针、调度信息
  • M(Machine):OS 线程的封装。G 必须绑定到 M 才能执行
  • P(Processor):调度上下文——持有 G 的本地运行队列。数量 = GOMAXPROCS

核心公式:M 的数量 ≥ GOMAXPROCS,P 的数量 = GOMAXPROCS。任何时刻最多有 GOMAXPROCS 个 G 在并行执行。

卷三第 08 篇 GMP 协程调度器机制 会深入 GMP 的调度循环、工作窃取、抢占式调度。


# 12.4 runtime.GOMAXPROCS 与 P 数量

GOMAXPROCS 决定了同时有多少个 goroutine 可以真正并行执行:

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println("CPU 核心数:", runtime.NumCPU())
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
    // 默认值 = CPU 核心数
}
1
2
3
4
5
6
7
8
9
10
11
12
# 环境变量覆盖
GOMAXPROCS=2 go run main.go

# 代码中设置
runtime.GOMAXPROCS(4)
1
2
3
4
5

什么时候调?

  • 默认不要调:GOMAXPROCS = NumCPU 是 Go 团队的优化结论
  • 容器环境可能需要调:如果 K8s 限制了 CPU 但 NumCPU 返回的是宿主机核数——用 GOMAXPROCS 或 automaxprocs 库
  • I/O 密集型可以大于 CPU 核数:让更多 G 有机会在 I/O 等待期间被调度

# 12.5 sync.WaitGroup:等所有 goroutine 完成

WaitGroup 是最简单的 goroutine 协调工具——等待一组 goroutine 完成任务:

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // ③ 完成时计数减一
    fmt.Printf("Worker %d 开始\n", id)
    // 模拟工作...
    fmt.Printf("Worker %d 完成\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)           // ① 启动前计数加一
        go worker(i, &wg)   // ② 启动 goroutine
    }

    wg.Wait()  // ④ 阻塞直到计数归零
    fmt.Println("所有 worker 完成")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

WaitGroup 的三步操作:

操作 谁调用 时机
wg.Add(n) 主 goroutine 启动 goroutine 之前
wg.Done() worker goroutine 工作完成时(通常 defer)
wg.Wait() 主 goroutine 等待所有 worker 完成

⚠️ 常见错误:

// ❌ 错误 1:Add 在 goroutine 内部——可能还没 Add 就 Wait 返回了
go func() {
    wg.Add(1)  // 太晚了!
    defer wg.Done()
    // ...
}()
wg.Wait()  // 可能 Add 没执行就通过了

// ✅ 正确:Add 在 go 之前
wg.Add(1)
go func() {
    defer wg.Done()
    // ...
}()
wg.Wait()

// ❌ 错误 2:忘记传指针——每个 goroutine 拿了 WaitGroup 的副本
func worker(wg sync.WaitGroup) {  // 值拷贝!
    defer wg.Done()
}
// → 永远不会 Done 到原始的 WaitGroup

// ✅ 正确:传指针
func worker(wg *sync.WaitGroup) {
    defer wg.Done()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 12.6 context 包:取消、超时、传值

context.Context 是 Go 并发程序的"信号管道"——在 goroutine 之间传递取消信号、超时、请求级别的数据。

# 12.6.1 四种 Context

import "context"

// ① 根 Context——不取消、不超时、不值
ctx := context.Background()   // 最常用
ctx := context.TODO()         // 不确定用什么时占位

// ② 可取消——主动调用 cancel() 通知所有下游
ctx, cancel := context.WithCancel(context.Background())
defer cancel()  // ★ 必须调用——否则资源泄漏

// ③ 超时——到时间自动取消
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

// ④ 截止时间——在指定时刻自动取消
deadline := time.Now().Add(10 * time.Minute)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

// ⑤ 传值——携带请求级别的数据(慎用)
ctx = context.WithValue(ctx, "trace_id", "abc123")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

defer cancel() 的必要性:

// ❌ 没有 cancel——即使超时已过,context 内部资源不释放
ctx, _ := context.WithTimeout(parent, time.Second)

// ✅ 标准写法——defer cancel 确保资源释放
ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel()
1
2
3
4
5
6

# 12.6.2 context.WithCancelCause(Go 1.20+)

Go 1.20 引入的 WithCancelCause 让取消带上原因:

var ErrTimeout = errors.New("处理超时")

ctx, cancel := context.WithCancelCause(context.Background())

go func() {
    time.Sleep(3 * time.Second)
    cancel(ErrTimeout)  // 取消并带上原因
}()

select {
case <-ctx.Done():
    fmt.Println("取消原因:", context.Cause(ctx))  // 处理超时
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 12.6.3 取消传播链

一个 context 取消,所有子 context 全部取消——这是一棵树:

Background
   │
   ├── WithTimeout(30s)    ← 请求级超时
   │      │
   │      ├── WithTimeout(3s)   ← DB 查询超时
   │      ├── WithTimeout(5s)   ← HTTP 调用超时
   │      └── WithCancel        ← 某步骤失败,手动取消剩余步骤
   │
   └── WithValue("trace_id")    ← 跨请求传递
1
2
3
4
5
6
7
8
9
func handleRequest(parent context.Context) {
    // 请求级超时 30 秒
    ctx, cancel := context.WithTimeout(parent, 30*time.Second)
    defer cancel()

    // 三个子任务——任意一个失败/超时 → cancel → 其他子任务感知
    ctx1, cancel1 := context.WithCancel(ctx)
    ctx2, cancel2 := context.WithCancel(ctx)

    go fetchFromDB(ctx1)
    go fetchFromCache(ctx2)

    // 等第一个返回
    // → 另一个的 ctx.Done() 立刻触发 → 清理
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 12.7 goroutine 泄漏的 5 种模式

goroutine 泄漏 = goroutine 永远阻塞在某个地方,无法退出。

# 12.7.1 模式 1:channel 无人接收

// ❌ goroutine 往一个无缓冲 channel 发数据——但外面没有接收者
func leak() {
    ch := make(chan int)
    go func() {
        ch <- 42  // 永远阻塞在这——因为没人读
    }()
    // ch 没有接收者 → goroutine 泄漏
}
1
2
3
4
5
6
7
8

# 12.7.2 模式 2:channel 无人发送

// ❌ goroutine 从一个 channel 读数据——但外面不会发
func leak() {
    ch := make(chan int)
    go func() {
        val := <-ch  // 永远阻塞在这——因为没人发
        fmt.Println(val)
    }()
    // ch 没有发送者 → goroutine 泄漏
}
1
2
3
4
5
6
7
8
9

# 12.7.3 模式 3:没有设置超时

// ❌ HTTP 请求没有超时——服务端卡住 → goroutine 永远等待
func fetch(url string) {
    resp, err := http.Get(url)  // 永远不会返回
    // ...
}

// ✅ 带超时
func fetchWithTimeout(url string) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := http.DefaultClient.Do(req)
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 12.7.4 模式 4:死锁(循环依赖)

// ❌ 两个 goroutine 互相等待——永远解不开
func deadlock() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        <-ch1
        ch2 <- 1  // 等 ch1 —但 ch1 没人发
    }()

    go func() {
        <-ch2
        ch1 <- 1  // 等 ch2 —但 ch2 没人发
    }()
}
// → fatal error: all goroutines are asleep - deadlock!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 12.7.5 模式 5:select 永远走不到退出分支

// ❌ select 没有 ctx.Done() 分支 → 永远退不出去
func leak(ctx context.Context) {
    for {
        select {
        case data := <-dataCh:
            process(data)
        // 没有 case <-ctx.Done()——永远退不出循环
        }
    }
}

// ✅ 加上退出分支
func noLeak(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return  // ← 退出路径
        case data := <-dataCh:
            process(data)
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 12.8 主 goroutine 退出 = 程序退出

Go 程序的退出规则:当主 goroutine(main 函数)返回时,所有其他 goroutine 立即被杀死——不管它们在做什么。

func main() {
    go func() {
        time.Sleep(5 * time.Second)
        fmt.Println("这条永远不会打印")
    }()

    fmt.Println("main 退出")
    // main 返回 → 程序退出 → goroutine 被杀死
}
1
2
3
4
5
6
7
8
9

正确做法——主 goroutine 需要等其他 goroutine:

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            time.Sleep(time.Duration(id) * time.Second)
            fmt.Printf("Worker %d 完成\n", id)
        }(i)
    }

    wg.Wait()  // ★ 等所有 worker
    fmt.Println("所有 worker 完成,程序退出")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

注意:panic 在子 goroutine 中不会影响主 goroutine——除非用 recover 捕获。


# 12.9 综合示例:并发抓取 N 个 URL(带超时)

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "sync"
    "time"
)

type Result struct {
    URL      string
    BodySize int
    Err      error
}

// 并发抓取——带超时 + 取消传播 + 结果合并
func fetchAll(ctx context.Context, urls []string) []Result {
    // 每个请求的超时——比总超时短
    reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()

    var wg sync.WaitGroup
    results := make([]Result, len(urls))

    for i, url := range urls {
        wg.Add(1)
        go func(i int, url string) {
            defer wg.Done()
            results[i] = fetchOne(reqCtx, url)
        }(i, url)
    }

    wg.Wait()
    return results
}

func fetchOne(ctx context.Context, url string) Result {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return Result{URL: url, Err: err}
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return Result{URL: url, Err: err}
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return Result{URL: url, Err: err}
    }

    return Result{URL: url, BodySize: len(body)}
}

func main() {
    urls := []string{
        "https://go.dev",
        "https://golang.org",
        "https://pkg.go.dev",
    }

    // 总超时 30 秒
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    results := fetchAll(ctx, urls)

    for _, r := range results {
        if r.Err != nil {
            fmt.Printf("❌ %s: %v\n", r.URL, r.Err)
        } else {
            fmt.Printf("✅ %s: %d bytes\n", r.URL, r.BodySize)
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

程序流程:

  1. 主 goroutine 遍历 URL 列表——为每个 URL 启动一个 goroutine
  2. 所有 goroutine 并发执行 fetchOne——WaitGroup 等它们全部完成
  3. 每个请求有独立的 10s 超时(子 context)
  4. 整个任务有 30s 总超时(父 context)——超时后所有子请求自动取消
  5. 结果收集到切片——主 goroutine 打印汇总

# 12.10 本章底层原理(简介)

本章涉及的底层机制在**卷三"专栏博客"**中详细展开:

本章概念 卷三对应篇
go f() 如何被调度 08.GMP 协程调度器机制
G、M、P 的生命周期 08.GMP 协程调度器机制
连续栈扩容(2KB→1GB) 01.内存模型与栈堆布局
context 取消传播实现 23.上下文取消与传播
goroutine 泄漏排查 15.协程泄漏排查与修复
sync.WaitGroup 源码 10.sync 同步原语剖析
goroutine 抢占式调度 21.抢占式调度器原理

# 12.11 Go 新手陷阱 Top 5

① ❌ go func(i int) { use(i) }(i) 传参写错

// ❌ 闭包捕获循环变量——所有 goroutine 读到同一个 i
for i := 0; i < 5; i++ {
    go func() { fmt.Println(i) }()
}
// 输出: 5 5 5 5 5 (Go 1.21-)或每次的 i(Go 1.22+)

// ✅ 通过参数传入——全版本安全
for i := 0; i < 5; i++ {
    go func(i int) { fmt.Println(i) }(i)
}
1
2
3
4
5
6
7
8
9
10

② ❌ 主 goroutine 不等其他 goroutine 就退出

// ❌ main 退出 → 程序结束 → goroutine 被杀
go doWork()
// main 立即返回

// ✅ WaitGroup 或 channel 等待
var wg sync.WaitGroup
wg.Add(1)
go func() { defer wg.Done(); doWork() }()
wg.Wait()
1
2
3
4
5
6
7
8
9

③ ❌ context 存到 struct 字段——反模式

// ❌ context 不应该作为 struct 字段
type Server struct {
    ctx context.Context  // 反模式
}

// ✅ context 作为函数第一个参数
func (s *Server) Handle(ctx context.Context, req *Request)
1
2
3
4
5
6
7

④ ❌ WithTimeout / WithCancel 不调用 cancel()

// ❌ 计时器不回收——资源泄漏
ctx, _ := context.WithTimeout(parent, time.Second)

// ✅ defer cancel——即使超时已过也要调
ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel()
1
2
3
4
5
6

⑤ ❌ 共享变量没加锁

// ❌ 多个 goroutine 同时写 counter——数据竞争
var counter int
for i := 0; i < 100; i++ {
    go func() { counter++ }()
}

// ✅ 加锁 或 用 sync/atomic
var mu sync.Mutex
var counter int
mu.Lock()
counter++
mu.Unlock()

// 或 atomic
var counter atomic.Int64
counter.Add(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 12.12 思考题

  1. 为什么 go f() 是"非阻塞"的?如果想让调用方等待 goroutine 完成,有几种方式?
  2. GOMAXPROCS=1 时,多个 goroutine 是"并发"还是"并行"?GOMAXPROCS=4 呢?
  3. WaitGroup 和 errgroup(golang.org/x/sync/errgroup)有什么区别?什么时候该换 errgroup?
  4. 写一个程序:用 WithTimeout 启动 3 个 goroutine 并发下载文件,任意一个超时则取消另外两个。
  5. 为什么 Go 不让 recover 跨 goroutine 生效?(提示:栈隔离)

# 12.13 推荐阅读

  • 入门卷:第 13 章 channel——channel 是 goroutine 间的通信工具
  • 入门卷:第 14 章 sync 包——Mutex、RWMutex、Once、Pool
  • 卷三:08.GMP 协程调度器机制——GMP 三者的协作全流程
  • 卷三:23.上下文取消与传播——context 的底层实现
  • 卷三:15.协程泄漏排查与修复——pprof 分析泄漏 + 5 种修复模式
  • Go Concurrency Patterns - Rob Pike (opens new window)——经典演讲
  • The Go Memory Model (opens new window)——happens-before 规则
上次更新: 2026/06/14, 15:49:50
错误处理
通道channel

← 错误处理 通道channel→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式