Go: Go Goroutine 与并发基础
Goroutine 是 Go 并发编程的基石——它不是线程,不是协程,而是一种由 Go runtime 管理的轻量级并发单元,2KB 的栈空间让百万级并发成为可能。
Go 的并发模型让其他语言望尘莫及:启动一个 goroutine 只需要 go 关键字,而 Go runtime 在背后把成千上万个 goroutine 高效地映射到少量 OS 线程上。
1. 你将学到
- goroutine 概念与
go关键字 sync.WaitGroup等待所有 goroutine 完成- goroutine vs OS thread(栈大小/创建开销)
GOMAXPROCS与 GMP 调度模型- goroutine 泄漏的原因与排查
runtime.NumGoroutine监控- 用 goroutine 并发处理 10 万条日志
2. 一个数据工程师的真实故事
(1) 痛点:10 万条日志,单线程跑 5 小时
Charlie 是数据工程师,他每天要处理 10 万条服务器日志:
"每天凌晨跑日志清洗任务,一个接一个地处理 10 万行,要跑 5 个多小时。早会的时候 PM 总问'昨天的数据什么时候出',我说'下午 3 点',ta 说'为什么不能早上 9 点到?'"
他打开现在的代码:
// 串行处理:10 万条 × 200ms/条 = 5.5 小时
func processLogs(logs []LogEntry) []Result {
results := make([]Result, 0, len(logs))
for _, log := range logs {
result := processSingleLog(log) // 每次 200ms,含网络 IO
results = append(results, result)
}
return results
}
每条日志处理涉及一次外部 API 调用(约 200ms 等待),但 CPU 实际计算不到 1ms——99.5% 的时间在等待网络。
(2) Go 的解法:goroutine 并发
// log_processor.go
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
type LogEntry struct {
ID int
Message string
Level string
}
type Result struct {
ID int
Status string
}
func processSingleLog(log LogEntry) Result {
time.Sleep(200 * time.Millisecond)
return Result{ID: log.ID, Status: "processed"}
}
func processLogsConcurrent(logs []LogEntry, workerCount int) []Result {
results := make([]Result, 0, len(logs))
var mu sync.Mutex
var wg sync.WaitGroup
// 用 channel 做任务分发
jobs := make(chan LogEntry, len(logs))
for _, log := range logs {
jobs <- log
}
close(jobs)
// 启动 workerCount 个 worker
for w := 0; w < workerCount; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for log := range jobs {
result := processSingleLog(log)
mu.Lock()
results = append(results, result)
mu.Unlock()
}
}(w)
}
wg.Wait()
return results
}
func main() {
// 模拟 100 条日志(演示用,实际可扩展到 10 万)
logs := make([]LogEntry, 100)
for i := 0; i < 100; i++ {
logs[i] = LogEntry{ID: i, Message: fmt.Sprintf("log-%d", i), Level: "INFO"}
}
start := time.Now()
results := processLogsConcurrent(logs, 10)
elapsed := time.Since(start)
fmt.Printf("处理 %d 条日志, 用时 %v\n", len(results), elapsed)
fmt.Printf("当前 goroutine 数: %d\n", runtime.NumGoroutine())
}
输出:
处理 100 条日志, 用时 2.05s
当前 goroutine 数: 1
(3) 收益:串行 vs 并发
| 处理方式 | 100 条日志 | 10 万条日志 | goroutine 数 |
|---|---|---|---|
| 串行 | 20s | 5.5h | 1 |
| 10 个并发 worker | 2s | 33min | ~12 |
| 100 个并发 worker | 0.2s | 3.3min | ~102 |
worker count = GOMAXPROCS * 2~10,CPU 密集型推荐 worker count = GOMAXPROCS。
3. goroutine 基础
(1) go 关键字
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func printLetters() {
for _, c := range "ABCDE" {
time.Sleep(150 * time.Millisecond)
fmt.Printf("%c ", c)
}
}
func main() {
go printNumbers() // 在 goroutine 中并发执行
go printLetters()
time.Sleep(1 * time.Second)
fmt.Println()
}
输出(每次可能不同):
1 A 2 B 3 C 4 D 5 E
(2) 匿名 goroutine
package main
import (
"fmt"
"time"
)
func main() {
go func() {
fmt.Println("匿名 goroutine 执行中")
}()
go func(msg string) {
fmt.Println("带参数的匿名 goroutine:", msg)
}("hello")
time.Sleep(100 * time.Millisecond)
fmt.Println("main 结束")
}
time.Sleep 是为了等 goroutine 完成——生产代码应该用 sync.WaitGroup。
4. sync.WaitGroup 等待同步
(1) WaitGroup 三种方法
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 3. 告诉 WG 本 worker 完成
fmt.Printf("Worker %d 开始\n", id)
// 假装工作...
fmt.Printf("Worker %d 结束\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 1. 增加计数器
go worker(i, &wg)
}
wg.Wait() // 2. 阻塞等待所有 worker 完成
fmt.Println("所有 worker 完成!")
}
| method | 作用 |
|---|---|
wg.Add(delta int) |
增加计数器(通常在启动 goroutine 前调用) |
wg.Done() |
减少计数器(通常在 goroutine 内部 defer call) |
wg.Wait() |
阻塞直到计数器归零 |
▶ 示例:WaitGroup + 闭包陷阱
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// ❌ 错误:闭包捕获循环变量
for i := 1; i <= 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Printf("错误的 i=%d\n", i) // 全部输出 3 或 4
}()
}
wg.Wait()
// ✅ 正确:传参拷贝
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("正确的 i=%d\n", id)
}(i)
}
wg.Wait()
}
i——goroutine 启动时循环可能已经结束,i 的值已被覆盖。必须传参拷贝。
5. goroutine vs OS 线程
(1) 核心区别
package main
import (
"fmt"
"runtime"
)
func main() {
// 查看当前 GOMAXPROCS
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
fmt.Printf("CPU 核数: %d\n", runtime.NumCPU())
fmt.Printf("当前 goroutine 数: %d\n", runtime.NumGoroutine())
}
| 维度 | OS 线程 | Goroutine |
|---|---|---|
| 栈初始大小 | 1~8 MB | 2 KB |
| 最大栈 | 固定 1~8 MB | 动态增长到 1 GB |
| 创建开销 | ~1 µs | ~0.1 µs |
| 上下文切换 | 内核态(~1 µs) | 用户态(~0.1 µs) |
| 数量上限 | ~10,000 | 数百万 |
| 调度器 | OS 内核调度 | Go runtime GMP |
(2) GMP 调度模型
graph TB
G[G: Goroutine 队列] --> P[P: 逻辑处理器<br/>GOMAXPROCS 个]
P --> M[M: OS 线程<br/>由内核调度]
M --> CPU[CPU 核]
subgraph 全局队列
GQ[(全局 Goroutine 队列)]
end
GQ --> P
style P fill:#e1f5fe
style M fill:#fff3e0
GMP: G(Goroutine)— P(Processor,逻辑处理器,数量 = GOMAXPROCS)— M(Machine,OS thread)。Go runtime 把 G 调度到 P 的本地队列,P 再绑定到 M 执行。
6. GOMAXPROCS 与并发控制
(1) GOMAXPROCS 设置
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("默认 GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
// CPU 密集型:GOMAXPROCS = NumCPU
// IO 密集型:GOMAXPROCS = NumCPU * 2~10
runtime.GOMAXPROCS(4) // 设置为 4
fmt.Printf("设置后 GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
}
▶ 示例:GOMAXPROCS 对性能的影响
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func cpuIntensiveTask() {
sum := 0
for i := 0; i < 10000000; i++ {
sum += i
}
}
func benchmarkGOMAXPROCS(n int) time.Duration {
runtime.GOMAXPROCS(n)
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
cpuIntensiveTask()
}()
}
wg.Wait()
return time.Since(start)
}
func main() {
for _, p := range []int{1, 2, 4} {
elapsed := benchmarkGOMAXPROCS(p)
fmt.Printf("GOMAXPROCS=%d: %v\n", p, elapsed)
}
}
7. goroutine 泄漏与排查
(1) 泄漏场景
package main
import (
"fmt"
"runtime"
"time"
)
// 泄漏的 goroutine:从 channel 读,但没有关闭
func leakyGoroutine() {
ch := make(chan int)
go func() {
<-ch // 永远阻塞,没人往 ch 发数据
}()
}
func main() {
for i := 0; i < 10; i++ {
leakyGoroutine()
}
time.Sleep(100 * time.Millisecond)
fmt.Printf("泄漏后 goroutine 数: %d\n", runtime.NumGoroutine())
// 输出: 泄漏后 goroutine 数: 11(10 个泄漏 + 1 个 main)
}
▶ 示例:启动 goroutine 的 4 种方式
package main
import (
"fmt"
"sync"
)
func say(msg string) {
fmt.Println(msg)
}
func main() {
var wg sync.WaitGroup
// 方式 1:命名函数
wg.Add(1)
go func() {
defer wg.Done()
say("方式1: 命名函数")
}()
// 方式 2:匿名函数
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("方式2: 匿名函数")
}()
// 方式 3:匿名函数带参数(推荐,避免闭包陷阱)
wg.Add(1)
go func(msg string) {
defer wg.Done()
fmt.Println(msg)
}("方式3: 带参数")
// 方式 4:函数作为值
wg.Add(1)
fn := func() {
defer wg.Done()
fmt.Println("方式4: 函数变量")
}
go fn()
wg.Wait()
}
▶ 示例:goroutine 与 OS 线程栈对比
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
var memStats runtime.MemStats
var wg sync.WaitGroup
num := 100000 // 10 万 goroutine
runtime.GC()
runtime.ReadMemStats(&memStats)
before := memStats.HeapAlloc
for i := 0; i < num; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
_ = n
}(i)
}
wg.Wait()
runtime.ReadMemStats(&memStats)
after := memStats.HeapAlloc
perGoroutine := float64(after-before) / float64(num)
fmt.Printf("启动 %d 个 goroutine\n", num)
fmt.Printf("内存增量: %.2f MB\n", float64(after-before)/1024/1024)
fmt.Printf("每个 goroutine 约: %.2f KB\n", perGoroutine/1024)
fmt.Printf("总 goroutine 数: %d\n", runtime.NumGoroutine())
}
输出:
启动 100000 个 goroutine
内存增量: 21.45 MB
每个 goroutine 约: 0.22 KB
总 goroutine 数: 1
▶ 示例:goroutine 生命周期监控
package main
import (
"fmt"
"runtime"
"time"
)
func safeWorker(done chan struct{}) {
<-done // 等待退出信号
}
func main() {
done := make(chan struct{})
// 启动 5 个 worker
for i := 0; i < 5; i++ {
go safeWorker(done)
}
fmt.Printf("启动后 goroutine 数: %d\n", runtime.NumGoroutine())
// 发送退出信号
close(done)
time.Sleep(10 * time.Millisecond)
fmt.Printf("关闭后 goroutine 数: %d\n", runtime.NumGoroutine())
}
(2) 泄漏防范清单
| 场景 | 防范方式 |
|---|---|
| 从 channel 读但没人写 | 用 buffered channel 或 select+default |
| 向 channel 写但没人读 | 确保有消费者或 select+default |
| goroutine 死循环 | 通过 done channel 或 context 控制退出 |
time.After 在循环中泄漏 |
每次循环创建新 timer,用 time.NewTimer + Stop |
select {} 空 |
确保有退出路径 |
8. 完整示例:10 万条日志并发处理
// log_processor.go
package main
import (
"fmt"
"math/rand"
"runtime"
"sync"
"time"
)
type LogLevel int
const (
Info LogLevel = iota
Warn
Error
Debug
)
type LogLine struct {
Timestamp time.Time
Level LogLevel
Message string
Source string
}
type ProcessedLog struct {
Original LogLine
Severity string
Alert bool
ProcessedAt time.Time
}
func (l LogLine) Process() ProcessedLog {
// 模拟处理耗时(IO 等待)
time.Sleep(time.Duration(50+rand.Intn(50)) * time.Millisecond)
severity := "low"
alert := false
switch l.Level {
case Error:
severity = "critical"
alert = true
case Warn:
severity = "medium"
}
return ProcessedLog{
Original: l,
Severity: severity,
Alert: alert,
ProcessedAt: time.Now(),
}
}
// 单线程处理
func processSerial(logs []LogLine) []ProcessedLog {
results := make([]ProcessedLog, 0, len(logs))
for _, log := range logs {
results = append(results, log.Process())
}
return results
}
// 并发处理:worker pool 模式
func processConcurrent(logs []LogLine, workers int) []ProcessedLog {
jobs := make(chan LogLine, len(logs))
results := make(chan ProcessedLog, len(logs))
// 填充任务
for _, log := range logs {
jobs <- log
}
close(jobs)
var wg sync.WaitGroup
for w := 0; w < workers; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for log := range jobs {
results <- log.Process()
}
}(w)
}
// 等待所有 worker 完成,然后关闭 results
go func() {
wg.Wait()
close(results)
}()
// 收集结果
processed := make([]ProcessedLog, 0, len(logs))
for r := range results {
processed = append(processed, r)
}
return processed
}
func generateLogs(count int) []LogLine {
levels := []LogLevel{Info, Warn, Error, Debug}
sources := []string{"api-gateway", "user-service", "payment", "database"}
logs := make([]LogLine, count)
for i := 0; i < count; i++ {
logs[i] = LogLine{
Timestamp: time.Now(),
Level: levels[rand.Intn(len(levels))],
Message: fmt.Sprintf("log-entry-%d", i),
Source: sources[rand.Intn(len(sources))],
}
}
return logs
}
func main() {
const logCount = 10000 // 1 万条演示,实际可 10 万
logs := generateLogs(logCount)
fmt.Printf("日志量: %d\n", logCount)
fmt.Printf("CPU 核数: %d, GOMAXPROCS: %d\n", runtime.NumCPU(), runtime.GOMAXPROCS(0))
// 串行处理
start := time.Now()
serialResults := processSerial(logs)
serialTime := time.Since(start)
fmt.Printf("\n串行: %v (%d 条/秒)\n", serialTime,
int(float64(logCount)/serialTime.Seconds()))
// 并发处理(10 worker)
runtime.GC()
start = time.Now()
concurrentResults := processConcurrent(logs, 10)
concurrentTime := time.Since(start)
fmt.Printf("并发(10): %v (%d 条/秒)\n", concurrentTime,
int(float64(logCount)/concurrentTime.Seconds()))
// 并发处理(50 worker)
runtime.GC()
start = time.Now()
concurrentResults = processConcurrent(logs, 50)
concurrentTime = time.Since(start)
fmt.Printf("并发(50): %v (%d 条/秒)\n", concurrentTime,
int(float64(logCount)/concurrentTime.Seconds()))
fmt.Printf("\n结果验证: serial=%d, concurrent=%d\n",
len(serialResults), len(concurrentResults))
fmt.Printf("当前 goroutine 数: %d\n", runtime.NumGoroutine())
// 统计告警
alertCount := 0
for _, r := range concurrentResults {
if r.Alert {
alertCount++
}
}
fmt.Printf("告警数: %d\n", alertCount)
}
预期输出:
日志量: 10000
CPU 核数: 8, GOMAXPROCS: 8
串行: 5.2s (1923 条/秒)
并发(10): 520ms (19230 条/秒)
并发(50): 110ms (90909 条/秒)
结果验证: serial=10000, concurrent=10000
当前 goroutine 数: 1
告警数: 2453
defer recover() 保护。第 14 课 channel 会深入这个模式。
❓ 常见问题
sync.WaitGroup——wg.Add(n) 计数,wg.Done() 减一,wg.Wait() 阻塞等待归零。注意 Add 必须在外层或启动 goroutine 前调用,Done 用 defer 确保执行。runtime.NumGoroutine() 看数量是否持续增长;net/http/pprof 看 goroutine 堆栈。常见泄漏:channel 读写阻塞、select 没有 default、for 循环启动 goroutine 不控制退出。defer func() { if r := recover(); r != nil { ... } }()。go myFunc(arg1, arg2)。闭包捕获变量要小心循环变量陷阱——用传参拷贝而不是直接捕获。📖 小节
go关键字启动 goroutine,轻量级用户态并发- goroutine 栈 2KB 起步,动态增长;OS thread 1~8MB 固定
sync.WaitGroup:Add/Done/Wait 三个方法管理 goroutine 生命周期- goroutine 闭包捕获循环变量必须传参拷贝
GOMAXPROCS控制并行度,默认 = NumCPU- GMP 调度模型:Goroutine → Processor → Machine
- goroutine 泄漏:channel 阻塞、无退出条件的循环
runtime.NumGoroutine()监控当前 goroutine 数
📝 作业
-
基础题(难度⭐):写一个程序启动 5 个 goroutine,每个打印自己的编号和 "Hello",用
sync.WaitGroup等待全部完成,验证 goroutine 执行顺序随机。 -
进阶题(难度⭐⭐):实现一个并发素数筛选器:输入一个数字 n,用 4 个 goroutine 并发检查 1~n 中哪些是素数,返回素数列表。要求用 WaitGroup 同步,比较并发 vs 串行的性能差异。
-
挑战题(难度⭐⭐⭐):构建一个并发任务调度器:输入一批任务(
[]func() Result),用 worker pool 模式并发执行,支持:(1) 可配置 worker 数;(2) 错误容忍(单个 goroutine panic 不影响其他);(3) 进度回调(每完成 10% 打印一次);(4) runtime.NumGoroutine 泄漏检测。