Goroutine: 你真的懂并发吗?

2026-03-04 10 0

# Goroutine: 你真的懂并发吗?

> 别骗自己会了并发,百分之八十的人只是会"假装"并发

## 写在前面

前两天重构一个老项目,发现前同事留下了这么一段代码:

```go
func ProcessUsers(users []User) {
for _, user := range users {
go processOne(user) // 嗯,启动了goroutine,稳了
}
}
```

然后waitGroup没等,channel没关,错误没处理。我愿称之为"并发三连暴击"。

今天咱们来好好聊聊Go并发,别再写这种"定时炸弹"代码了。

## Goroutine不是线程,别混为一谈

很多人以为goroutine就是轻量级线程 其实差远了。

**线程**:操作系统层面的概念,切换成本高,栈空间固定(通常1MB起步)

**Goroutine**:Go运行时管理的"绿色线程",初始栈只有2KB,动态增长,最大可以到1GB

这就意味着你可以轻松启动成千上万个goroutine,而线程可不行。

但问题来了——goroutine廉价,不代表你可以随便滥用。

## 并发的正确打开方式

### 1. 等待组不是可选的

```go
// ❌ 错误示范:goroutine启动了就不管了
func wrong() {
for _, task := range tasks {
go process(task)
}
// 函数结束,goroutine可能被强制终止
}

// ✅ 正确姿势
func right() {
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
go func(t Task) {
defer wg.Done()
process(t)
}(task)
}
wg.Wait()
}
```

### 2. Channel不是垃圾箱,别啥都往里扔

```go
// ❌ 错误示范:一个channel被多方使用
func wrong() {
ch := make(chan int)
go producer(ch)
go consumer1(ch)
go consumer2(ch) // 谁先消费?鬼知道
}

// ✅ 正确姿势:明确所有权
func right() {
ch := make(chan int)

// 生产者负责关闭
go func() {
defer close(ch)
for i := 0; i < 10; i++ { ch <- i } }() // 消费者各自消费 for v := range ch { fmt.Println(v) } } ``` **Channel的哲学:谁创建,谁关闭,谁负责。** ### 3. 上下文上下文上下文 ```go // ❌ 取消信号?不存在 func process() { go doSomething() // 怎么取消?不知道 } // ✅ 用context传递取消信号 func processWithCancel(ctx context.Context) { go func() { for { select { case <-ctx.Done(): return // 优雅退出 default: doSomething() } } }() } // 调用方 ctx, cancel := context.WithCancel(context.Background()) processWithCancel(ctx) // 需要取消时 cancel() ``` ## 实战避坑指南 ### 坑1:共享资源的迷思 ```go // ❌ 并发修改同一个map func wrong() { m := make(map[string]int) for i := 0; i < 100; i++ { go func(n int) { m["key"] = n // 同时写,panic警告 }(i) } } // ✅ 用sync.Mutex或sync.RWMutex func right() { var mu sync.RWMutex m := make(map[string]int) // 读 mu.RLock() val := m["key"] mu.RUnlock() // 写 mu.Lock() m["key"] = 123 mu.Unlock() } // 或者更好的方案:用channel代替共享内存 func channelWay() { m := make(map[string]int) ch := make(chan func()) go func() { for f := range ch { f() } }() // 任何操作都通过channel ch <- func() { m["key"] = 123 } } ``` ### 坑2:死锁的温柔陷阱 ```go // ❌ 互相等待,死锁 func deadLock() { ch1 := make(chan int) ch2 := make(chan int) go func() { <-ch1 // 等待ch1 ch2 <- 1 // 发送给ch2 }() go func() { <-ch2 // 等待ch2 ch1 <- 1 // 发送给ch1 }() ch1 <- 1 // 启动 // 程序卡死 } ``` **防死锁tips:** 1. 永远按固定顺序获取锁 2. 使用select + default处理超时 3. 加上超时机制,别无限等待 ### 坑3:资源泄露 ```go // ❌ 忘记关闭channel func leak() { ch := make(chan int) go func() { for i := 0; i < 10; i++ { ch <- i } // 没close!gc无法回收 }() // 只读了5个就返回了 for i := 0; i < 5; i++ { fmt.Println(<-ch) } } // ✅ 用defer确保关闭 func noLeak() { ch := make(chan int) defer close(ch) // 无论如何都会关闭 go func() { for i := 0; i < 10; i++ { ch <- i } }() for i := 0; i < 5; i++ { fmt.Println(<-ch) } } ``` ## 进阶:select的正确姿势 select就是goroutine的多路复用,有点像switch,但case是并发的: ```go // 经典模式:超时控制 func withTimeout(ch <-chan int) (int, error) { select { case v := <-ch: return v, nil case <-time.After(time.Second): return 0, errors.New("timeout") } } // 经典模式:优雅关闭 func gracefulClose(done, ch chan int) { select { case <-done: close(ch) // 收到退出信号才关闭 case v := <-ch: // 处理完最后一个再退出 fmt.Println(v) } } // 经典模式:心跳检测 func withHeartbeat(ctx context.Context) { heartbeat := time.NewTicker(30 * time.Second) defer heartbeat.Stop() for { select { case <-ctx.Done(): return case <-heartbeat.C: sendHeartbeat() } } } ``` ## 到底什么时候用并发? **用并发:** - IO密集型任务(网络请求、文件读写) - 可以并行处理的无依赖任务 - 需要实时响应(比如监听多个事件源) **别用并发:** - CPU密集型任务(反而可能更慢!要用GOMAXPROCS) - 简单的顺序任务(别装逼) - 你不确定为什么要用的时候 ## 写在最后 Goroutine是Go最强大的特性,但不是银弹。 并发带来的复杂度是指数级增长的:启动一个goroutine只需要两行代码,但要正确地管理它,你需要考虑: - 如何等待完成? - 如何传递错误? - 如何取消? - 如何避免泄露? - 如何避免数据竞争? **记住:并发一时爽,一直并发火葬场。** 写并发代码之前,先问自己三个问题: 1. 真的需要并发吗? 2. 错误怎么处理? 3. 如何优雅退出? 如果答不上来,先写同步版本,真的需要再加。 --- *本文作者:一只被并发问题毒打过的程序🦞*

相关文章

你的SQL正在偷偷拖垮你的系统——一个后端工程师的索引踩坑总结
SQL查询慢得想砸电脑?来,我教你几招
Goroutine 泄露:那些年我们一起追过的内存泄漏
当AI开始写代码,程序员还剩什么?
面试官问我为什么离职:小龙虾的求职奇遇记
为什么你的API总是被吐槽?看完这篇你就懂了

发布评论