目录
- [🟡 Go 入门到精通:Goroutine 入门 ------ 轻量级并发的秘密](#🟡 Go 入门到精通:Goroutine 入门 —— 轻量级并发的秘密)
-
- [1. 开篇:Go 并发的灵魂](#1. 开篇:Go 并发的灵魂)
- [2. goroutine 是什么?](#2. goroutine 是什么?)
-
- [2.1 一句话定义](#2.1 一句话定义)
- [2.2 核心特征](#2.2 核心特征)
- [3. go 关键字:一行代码启动并发](#3. go 关键字:一行代码启动并发)
-
- [3.1 基本语法](#3.1 基本语法)
- [3.2 闭包陷阱](#3.2 闭包陷阱)
- [4. 主 goroutine 退出即结束](#4. 主 goroutine 退出即结束)
- [5. sync.WaitGroup:等待协程完成](#5. sync.WaitGroup:等待协程完成)
-
- [5.1 基本用法](#5.1 基本用法)
- [5.2 WaitGroup 使用原则](#5.2 WaitGroup 使用原则)
- [5.3 WaitGroup 最佳实践模式](#5.3 WaitGroup 最佳实践模式)
- [6. goroutine vs 线程:全方位对比](#6. goroutine vs 线程:全方位对比)
-
- [6.1 对比表](#6.1 对比表)
- [6.2 规模演示](#6.2 规模演示)
- [7. GMP 调度模型深度解析](#7. GMP 调度模型深度解析)
-
- [7.1 GMP 三元素](#7.1 GMP 三元素)
- [7.2 GMP 调度流程](#7.2 GMP 调度流程)
- [7.3 工作窃取(Work Stealing)图示](#7.3 工作窃取(Work Stealing)图示)
- [7.4 Goroutine 生命周期](#7.4 Goroutine 生命周期)
- [8. GOMAXPROCS:并发度的控制旋钮](#8. GOMAXPROCS:并发度的控制旋钮)
-
- [8.1 基本概念](#8.1 基本概念)
- [8.2 GOMAXPROCS 的影响](#8.2 GOMAXPROCS 的影响)
- [8.3 并发 vs 并行](#8.3 并发 vs 并行)
- [9. runtime 包:窥探运行时](#9. runtime 包:窥探运行时)
-
- [9.1 runtime 常用函数速查](#9.1 runtime 常用函数速查)
- [10. goroutine 不是银弹:陷阱与反思](#10. goroutine 不是银弹:陷阱与反思)
-
- [10.1 过度创建 goroutine](#10.1 过度创建 goroutine)
- [10.2 goroutine 泄漏](#10.2 goroutine 泄漏)
- [10.3 Goroutine 使用清单](#10.3 Goroutine 使用清单)
- [11. 实战:并发爬虫框架](#11. 实战:并发爬虫框架)
- [12. 小结与互动](#12. 小结与互动)
-
- 核心要点回顾
- [🧠 GMP 结构记忆](#🧠 GMP 结构记忆)
- [📝 思考题](#📝 思考题)
- [💬 互动](#💬 互动)
🟡 Go 入门到精通:Goroutine 入门 ------ 轻量级并发的秘密
📅 更新于 2026年7月 | ✍️ 原创文章,转载请注明出处
1. 开篇:Go 并发的灵魂
如果说 Go 语言有什么"杀手级特性",那 goroutine 必然排在第一位。在 Go 中启动一个并发任务只需要一个 go 关键字:
go
go doSomething() // 搞定!
而在传统线程模型中,这意味着几十 KB 的栈空间分配、内核线程创建、复杂的线程池管理......
这篇文章将带你从零开始理解 goroutine------从它的轻量本质到 GMP 调度模型,从基本用法到高级陷阱,为后面的 channel 学习打下坚实基础。
2. goroutine 是什么?
2.1 一句话定义
goroutine 是 Go 运行时管理的轻量级用户态线程(协程),初始栈大小仅 2KB,由 Go 调度器(GMP模型)在操作系统线程上复用调度。
2.2 核心特征
go
// goroutine 的四个核心特征:
// 1️⃣ 轻量:初始栈仅 2KB(线程通常是 1-8MB)
// 2️⃣ 动态栈:栈空间按需伸缩(最大可达 1GB)
// 3️⃣ 用户态调度:不由 OS 内核调度,切换成本极低(~200ns vs 线程 ~1-10µs)
// 4️⃣ 与 Go 运行时深度集成:GC、channel、select 等原生支持
| 特性 | goroutine | OS 线程 |
|---|---|---|
| 初始栈大小 | ~2KB | ~1-8 MB |
| 栈增长方式 | 动态伸缩 | 固定大小 |
| 创建开销 | 数微秒 | 数百微秒 ~ 毫秒 |
| 切换开销 | ~200 ns | ~1-10 µs |
| 调度方式 | Go 运行时(用户态) | OS 内核 |
| 数量上限 | 数十万 ~ 百万 | 数千 |
| 标识 | 无内置 ID | 有 TID |
3. go 关键字:一行代码启动并发
3.1 基本语法
go
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello from goroutine!")
}
func main() {
// 启动一个 goroutine
go sayHello()
// 匿名函数 + goroutine
go func() {
fmt.Println("Hello from anonymous goroutine!")
}()
// 带参数的 goroutine
go func(name string) {
fmt.Printf("Hello, %s!\n", name)
}("Go 2026")
// ⚠️ 必须等待:否则 main 退出,goroutine 来不及执行
time.Sleep(100 * time.Millisecond)
}
3.2 闭包陷阱
go
func main() {
// ❌ 经典错误:闭包捕获了循环变量
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // 可能全打印 5!
}()
}
time.Sleep(time.Second)
// 输出:5 5 5 5 5(大概率)
fmt.Println("---")
// ✅ 正确方式1:传参复制
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
time.Sleep(time.Second)
// ✅ 正确方式2:局部变量复制(Go 1.22+ 循环变量语义已改进)
for i := 0; i < 5; i++ {
i := i // 显式复制
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Second)
}
4. 主 goroutine 退出即结束
这是初学者最容易犯的错误之一:
go
func main() {
// ❌ 可能什么都看不到
go func() {
fmt.Println("这条消息可能永远不会被打印")
}()
// main 函数返回 → 进程退出 → 所有 goroutine 被强制终止
}
Go 程序的规则:
main函数本身运行在 main goroutine 中- main goroutine 退出 → 整个程序立即退出
- 不会等待其他 goroutine 完成
go
// 图解:
// main goroutine ──────────────────────▶ 退出 💀
// │
// go func() ───────┼── "正在执行..." ──▶ 被强制终止 ☠️
// │
// time ────────────▶
5. sync.WaitGroup:等待协程完成
sync.WaitGroup 是等待一组 goroutine 完成的标准工具,可以看作一个线程安全的计数器。
5.1 基本用法
go
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // ⭐ 确保计数器减一(即使 panic)
fmt.Printf("Worker %d 开始\n", id)
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
fmt.Printf("Worker %d 完成\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // 计数器 +1
go worker(i, &wg)
}
wg.Wait() // 阻塞直到计数器归零
fmt.Println("所有 Worker 完成!")
}
5.2 WaitGroup 使用原则
go
// ✅ 三条铁律:
// 1. Add 必须在 goroutine 启动前调用
// 2. Done 必须在 goroutine 内部调用(通常用 defer)
// 3. WaitGroup 必须通过指针传递(不能值拷贝)
// ❌ 错误示例1:Add 在 goroutine 内部
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
go func() {
wg.Add(1) // ❌ 可能在 Wait 之后调用
defer wg.Done()
// ...
}()
}
wg.Wait()
// ❌ 错误示例2:值传递
func wrongWorker(wg sync.WaitGroup) { // ❌ 值拷贝!
defer wg.Done()
}
// 应该传指针:func worker(wg *sync.WaitGroup)
5.3 WaitGroup 最佳实践模式
go
// 模式1:循环启动
func launchWorkers(n int) {
var wg sync.WaitGroup
wg.Add(n) // 一次性设置总数
for i := 0; i < n; i++ {
go func(id int) {
defer wg.Done()
doWork(id)
}(i)
}
wg.Wait()
}
// 模式2:逐个添加
func launchDynamic() {
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1) // 逐个添加
go func(t Task) {
defer wg.Done()
process(t)
}(task)
}
wg.Wait()
}
// 模式3:封装为工具函数
func ConcurrentExec(tasks []func()) {
var wg sync.WaitGroup
wg.Add(len(tasks))
for _, task := range tasks {
task := task // 复制
go func() {
defer wg.Done()
task()
}()
}
wg.Wait()
}
6. goroutine vs 线程:全方位对比
6.1 对比表
| 维度 | goroutine | OS 线程 (Java Thread) |
|---|---|---|
| 内存开销 | 初始 2KB,动态伸缩 | 固定 1-8MB |
| 创建速度 | ~2-3 µs | ~50-100 µs |
| 切换开销 | ~200 ns(用户态) | ~1-10 µs(内核态) |
| 调度器 | Go GMP 调度器 | OS 内核调度器 |
| 上下文切换 | 用户态切换,无需陷入内核 | 内核态切换,保存/恢复大量寄存器 |
| 数量规模 | 轻松百万级 | 通常几千(受内存限制) |
| 抢占方式 | 协作式 + 信号抢占(Go 1.14+) | 时间片抢占 |
| 标识 | 无 goroutine ID(故意隐藏) | 有 Thread ID |
| 局部存储 | 无(推荐 context 传递) | ThreadLocal |
6.2 规模演示
go
func main() {
// 启动 10 万个 goroutine(内存仅增加约 200MB)
var wg sync.WaitGroup
wg.Add(100_000)
for i := 0; i < 100_000; i++ {
go func(id int) {
defer wg.Done()
time.Sleep(time.Second)
}(i)
}
fmt.Println("10万个 goroutine 已启动!")
wg.Wait()
fmt.Println("全部完成")
}
如果用 OS 线程实现同样的功能:100,000 × 1MB = ~100GB 内存,基本不可行。
7. GMP 调度模型深度解析
7.1 GMP 三元素
Go 的并发调度模型称为 GMP 模型:
┌──────────────────────────────────────────┐
│ GMP 调度模型 │
│ │
│ G ── G ── G ── G ── G (Goroutine) │
│ │ │ │
│ P ─────── P ─────── P (Processor) │
│ │ │ │ │
│ M ─────── M ─────── M (Machine) │
│ │ │ │ │
│ [CPU] [CPU] [CPU] │
└──────────────────────────────────────────┘
| 元素 | 全称 | 含义 | 数量 |
|---|---|---|---|
| G | Goroutine | 用户态协程,承载任务 | 成千上万 |
| M | Machine | OS 线程,真正执行代码 | 动态变化 |
| P | Processor | 逻辑处理器,持有本地运行队列 | GOMAXPROCS(默认 CPU 核数) |
7.2 GMP 调度流程
go
// GMP 工作流程(简化版):
// 1. 每个 P 维护一个本地 goroutine 队列(runq)
// 2. P 与 M 绑定,M 从 P 的本地队列取 G 执行
// 3. 当 M 因系统调用阻塞时,P 会与 M 解绑,寻找新的 M
// 4. 当 M 的 G 阻塞在 channel/网络 I/O 时,G 被挂起,M 继续执行其他 G
// 5. 当 P 的本地队列为空时,从全局队列或"偷"其他 P 的 G
// 核心设计原则:
// - M 是计算资源,P 是调度资源,G 是任务
// - M:P 通常 1:1,但 P 可以在 M 之间迁移
// - 工作窃取(Work Stealing):空闲 P 从忙碌 P 偷 G
7.3 工作窃取(Work Stealing)图示
P1 (忙碌) P2 (空闲)
┌─────────┐ ┌─────────┐
│ G1 G2 G3│ │ (空) │
│ G4 G5 G6│ │ │
│ G7 G8 G9│ ── steal──▶│ G7 G8 G9 │
└─────────┘ └─────────┘
当 P2 的本地队列为空时,它会从 P1 "偷"一半的 goroutine。这种设计确保了负载均衡。
7.4 Goroutine 生命周期
┌──────┐ go func() ┌──────────┐ 执行完毕 ┌──────┐
│ 创建 │ ──────────▶ │ 可运行 │ ────────▶ │ 退出 │
└──────┘ └──────────┘ └──────┘
│
┌────────┼────────┐
▼ ▼ ▼
┌─────────┐ ┌────────┐ ┌──────────┐
│channel │ │ 网络I/O│ │ 系统调用 │
│等待 │ │ 等待 │ │ 阻塞 │
└─────────┘ └────────┘ └──────────┘
│ │ │
└────────┼────────┘
▼
┌──────────┐
│ 可运行 │ (等待重新调度)
└──────────┘
8. GOMAXPROCS:并发度的控制旋钮
8.1 基本概念
GOMAXPROCS 决定了同时执行 Go 代码的 OS 线程数量上限,即 P 的数量。
go
package main
import (
"fmt"
"runtime"
)
func main() {
// 查看当前值
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 默认 = CPU 核数
// 设置新值(返回旧值)
old := runtime.GOMAXPROCS(4)
fmt.Println("旧值:", old)
// 也可以通过环境变量设置:
// GOMAXPROCS=4 go run main.go
}
8.2 GOMAXPROCS 的影响
| GOMAXPROCS 值 | 效果 |
|---|---|
1 |
单核执行,无真正的并行,但 goroutine 仍可并发 |
N(N < CPU核数) |
限制并行度,为其他应用留出 CPU |
CPU核数(默认) |
充分利用多核 |
> CPU核数 |
无额外收益,增加调度开销 |
8.3 并发 vs 并行
并发(单核,GOMAXPROCS=1):
G1: ──▶──▶──
G2: ──▶──▶──
G3: ──▶──▶──
单个 CPU 交替执行
并行(多核,GOMAXPROCS=4):
CPU1: G1 ────────▶
CPU2: G2 ────────▶
CPU3: G3 ────────▶
CPU4: G4 ────────▶
同时执行
💡 Go 的哲学:Don't communicate by sharing memory; share memory by communicating.(不通过共享内存来通信,而通过通信来共享内存)------ 这将在下一篇 Channel 详解中展开。
9. runtime 包:窥探运行时
go
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// NumGoroutine:当前 goroutine 数量
fmt.Println("goroutine 数量:", runtime.NumGoroutine()) // 至少 1(main)
// NumCPU:逻辑 CPU 数量
fmt.Println("CPU 核心数:", runtime.NumCPU())
// GOMAXPROCS:当前 P 的数量
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
// Gosched:主动让出 CPU
go func() {
for i := 0; i < 5; i++ {
fmt.Println("goroutine A:", i)
runtime.Gosched() // 让出执行权
}
}()
go func() {
for i := 0; i < 5; i++ {
fmt.Println("goroutine B:", i)
runtime.Gosched()
}
}()
time.Sleep(time.Second)
// Goexit:终止当前 goroutine(但不影响其他)
go func() {
defer fmt.Println("defer 仍会执行")
runtime.Goexit() // 终止 goroutine(defer 会执行)
fmt.Println("这行不会执行")
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("main 结束,goroutine 数量:", runtime.NumGoroutine())
// LockOSThread:绑定当前 goroutine 到 OS 线程
// 适用于需要线程局部存储的 CGO 调用
// runtime.LockOSThread()
// defer runtime.UnlockOSThread()
}
9.1 runtime 常用函数速查
| 函数 | 用途 |
|---|---|
runtime.NumGoroutine() |
当前 goroutine 数量(调试用) |
runtime.NumCPU() |
逻辑 CPU 核心数 |
runtime.GOMAXPROCS(n) |
设置/查询 P 的数量 |
runtime.Gosched() |
主动让出 CPU 时间片 |
runtime.Goexit() |
终止当前 goroutine |
runtime.LockOSThread() |
绑定 goroutine 到 OS 线程 |
runtime.GC() |
手动触发 GC(慎用) |
runtime.ReadMemStats() |
读取内存统计 |
10. goroutine 不是银弹:陷阱与反思
10.1 过度创建 goroutine
go
// ❌ 坏习惯:每个请求创建一个无界数量的 goroutine
func handleRequest(w http.ResponseWriter, r *http.Request) {
items := fetchAllItems() // 可能有 10 万条
for _, item := range items {
go processItem(item) // 💣 瞬间创建 10 万个 goroutine!
}
}
// ✅ 正确做法:使用工作池(worker pool)或 semaphore 控制并发度
func handleRequestProperly(items []Item) {
const maxConcurrency = 100
sem := make(chan struct{}, maxConcurrency)
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
sem <- struct{}{} // 获取令牌
go func(it Item) {
defer wg.Done()
defer func() { <-sem }() // 释放令牌
processItem(it)
}(item)
}
wg.Wait()
}
10.2 goroutine 泄漏
go
// ❌ goroutine 泄漏:channel 永远没有接收者
func leak() {
ch := make(chan int)
go func() {
ch <- 42 // 永远阻塞!goroutine 泄漏
}()
// 忘记从 ch 读取
}
// ❌ 另一个泄漏场景:无限循环的 goroutine
func leak2() {
go func() {
for {
// 做点什么...但没有退出条件
time.Sleep(time.Second)
}
}()
// 这个 goroutine 永远不会退出
}
// ✅ 正确做法:提供退出机制
func noLeak(ctx context.Context) {
go func() {
for {
select {
case <-ctx.Done():
return // 正确退出
default:
// 做点什么
time.Sleep(time.Second)
}
}
}()
}
10.3 Goroutine 使用清单
| ✅ 应该使用 goroutine | ❌ 不应使用 goroutine |
|---|---|
| I/O 密集型任务(网络、文件、数据库) | CPU 密集型计算任务(超过 CPU 核数无收益) |
| 独立的并行任务(互不依赖) | 强依赖顺序的任务 |
| 需要异步执行的操作 | 简单的 1+1(杀鸡不用牛刀) |
| 高并发服务(每个连接一个 goroutine) | 无需并发的场景 |
| 定时/周期性任务 | 为了炫耀技术 |
11. 实战:并发爬虫框架
go
package main
import (
"fmt"
"sync"
"time"
)
// Crawler 并发爬虫
type Crawler struct {
maxWorkers int
}
func NewCrawler(maxWorkers int) *Crawler {
return &Crawler{maxWorkers: maxWorkers}
}
// Crawl 并发爬取 URL 列表
func (c *Crawler) Crawl(urls []string) map[string]string {
results := make(map[string]string)
var mu sync.Mutex
var wg sync.WaitGroup
// semaphore 控制并发数
sem := make(chan struct{}, c.maxWorkers)
for _, url := range urls {
wg.Add(1)
sem <- struct{}{}
go func(u string) {
defer wg.Done()
defer func() { <-sem }()
content := fetchURL(u) // 模拟抓取
mu.Lock()
results[u] = content
mu.Unlock()
}(url)
}
wg.Wait()
return results
}
func fetchURL(url string) string {
time.Sleep(200 * time.Millisecond) // 模拟网络延迟
return fmt.Sprintf("Content of %s", url)
}
func main() {
urls := make([]string, 20)
for i := 0; i < 20; i++ {
urls[i] = fmt.Sprintf("http://example.com/page/%d", i)
}
crawler := NewCrawler(5) // 最多 5 个并发
start := time.Now()
results := crawler.Crawl(urls)
elapsed := time.Since(start)
fmt.Printf("爬取了 %d 个 URL,耗时: %v\n", len(results), elapsed)
// 20 个 URL,5 并发 → 大约 4 轮 × 200ms = ~800ms
}
12. 小结与互动
核心要点回顾
| 知识点 | 记忆要点 |
|---|---|
| goroutine 概念 | 轻量级用户态协程,2KB 初始栈,GMP 调度 |
| go 关键字 | 一行启动并发,但需要 WaitGroup 等待 |
| WaitGroup | Add/Done/Wait,必须传指针 |
| GMP 模型 | G=协程 M=线程 P=逻辑处理器 |
| GOMAXPROCS | 控制 P 的数量,默认 CPU 核数 |
| Gosched | 主动让出,Goexit 终止当前 goroutine |
| goroutine 泄漏 | 永远阻塞无退出 → 内存泄漏 |
| 并发控制 | semaphore/worker pool 限制并发度 |
🧠 GMP 结构记忆
G → 要做什么(任务)
M → 谁来做(干活的人,OS线程)
P → 在哪做(工作台,有工具和队列)
M 必须绑定 P 才能执行 G
P 的数量 = GOMAXPROCS
📝 思考题
- 如果
GOMAXPROCS=1,程序还会并发吗?这与"并行"有何区别? - 如何检测和排查 goroutine 泄漏?(提示:
runtime.NumGoroutine()+ pprof) - 什么是"工作窃取"(Work Stealing)?为什么它比全局队列更高效?
💬 互动
你在项目中最多同时运行过多少个 goroutine?有没有遇到过 goroutine 泄漏的坑?欢迎在评论区分享你的并发经验!
👨💻 作者 :布朗克168
📚 系列 :Go 入门到精通 2026
上一篇:文件操作 | 下一篇:Channel 详解
如果你觉得这篇文章对你有帮助,别忘了点赞 👍、收藏 ⭐ 和关注!