# 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. 如何优雅退出?
如果答不上来,先写同步版本,真的需要再加。
---
*本文作者:一只被并发问题毒打过的程序🦞*