2、🧠 Go 的协程为什么比你还能扛?goroutine 深度解析

185 阅读3分钟

好的,以下是你的 Go 系列第二篇原创技术专家文章草稿:


🧠 Go 的协程为什么比你还能扛?goroutine 深度解析

如果你累了,就看看 goroutine:
它没薪资、没社保、不抱怨,还能每秒启动上万个。


一、什么是 goroutine?

Goroutine 是 Go 的超轻量线程。你只需要在函数前面加一个 go 关键字,就能把这个函数“扔出去”,让它异步执行:

go doSomething()

对,就是这么简单。没有 thread pool,也不需要 import 第三方库。像点外卖一样顺滑。


二、goroutine 到底“轻”在哪?

🧊 内存小得离谱

每个 goroutine 启动时只占用约 2KB 栈内存(对比 Java 的线程 1MB 起步)。所以你可以一口气起几万个也不崩。

for i := 0; i < 100000; i++ {
    go fmt.Println(i)
}

这个代码甚至不会把你的电脑烧了。原因在于它用了 协作式调度机制(不是 OS 调度)+ 自适应栈增长。


三、GPM 模型是什么?

这是 Go 的并发核心架构,也是 goroutine 能“扛事”的底层秘密。

名称解释类比
G(goroutine)要执行的任务(代码逻辑)打工人
P(processor)逻辑处理器,调度单位工头
M(machine)真实线程,跑在 CPU 上工位

G 是活儿,P 是管活儿的,M 是真正干活儿的线程。

调度流程简图:

G(任务) → 放进 P 的队列 → 分配给 M 执行

Go runtime 会动态控制 P 和 M 的数量,做到最大化 CPU 使用率。


四、goroutine 和 OS 线程有啥不同?

特性goroutineOS 线程
创建开销小,约 2KB大,约 1MB
启动速度纳秒级毫秒级
数量支持上万甚至百万几千就危险
调度方式Go 自带调度器操作系统调度器
阻塞行为可用 channel 避免阻塞即挂起

一句话总结:线程是砖头,goroutine 是泡沫板,又轻又多还抗震。


五、channel + goroutine = 并发天花板

你可能想问:“goroutine 多了不会乱套吗?”

不会,因为它和 channel 是官配组合。

ch := make(chan int)

go func() {
    ch <- 42
}()

fmt.Println(<-ch)

channel 保证了数据同步、安全传输,天然支持协程间通信,像 walkie-talkie 一样可靠。


六、如何正确使用 goroutine?

✅ 用 goroutine 的场景:

  • IO 操作(网络请求、磁盘读写)
  • 定时任务(定时器、心跳包)
  • 批量任务并发执行(爬虫、批处理)

❌ 不建议的用法:

  • 大量 goroutine 执行计算密集型任务,容易导致调度负载加剧
  • 忘记关闭 goroutine(泄漏!)

👇 示例:并发爬虫任务

urls := []string{"a.com", "b.com", "c.com"}
for _, url := range urls {
    go func(u string) {
        resp, _ := http.Get(u)
        fmt.Println(u, resp.Status)
    }(url)
}

🧩 常见坑:循环变量捕获

for _, v := range arr {
    go func() {
        fmt.Println(v) // 错!
    }()
}

上面会打印重复值,因为闭包捕获的是同一个 v 变量地址。应该改成:

for _, v := range arr {
    go func(val int) {
        fmt.Println(val)
    }(v)
}

🎁 彩蛋:如何检测 goroutine 泄漏?

runtime.NumGoroutine()

结合 pprof 工具可以进一步定位卡住协程的位置。


总结一句话:

goroutine 是 Go 给你的超能力,但能力越大,责任越大,别让它“野跑”了。