在别的语言里写并发?那叫“修仙”——动不动就死锁、竞态、内存爆炸。
但在 Go 里写并发?那叫“打牌”——Go 给你发了七张神卡,每一张都能让你稳赢!
今天我们就来拆解这七张“并发神卡”,手把手教你用 Go 轻松驾驭高并发!
🃏 第一张:轻量级协程 —— Goroutine
“启动一个 goroutine,比点外卖还快。”
go func() {
fmt.Println("Hello from a goroutine!")
}()
- 超轻量:每个 goroutine 初始栈仅 2KB,能自动伸缩。
- 超便宜:你可以轻松启动 10 万个 goroutine,而不会炸掉服务器(不像 C++ 线程,1000 个就喘不过气)。
- 但注意:主程序不会等你!别忘了用
sync.WaitGroup:
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("任务完成!")
}()
wg.Wait() // 等它跑完再退出
}
💡 生活类比:Goroutine 就像你点了 100 份外卖,但不用自己去取——Go 运行时会自动调度骑手(OS 线程)帮你送。
🃏 第二张:消息管道 —— Channel
“别抢内存了,咱们发消息吧!”
Go 的核心哲学:Don’t communicate by sharing memory; share memory by communicating.
ch := make(chan int)
go func() { ch <- 42 }() // 发送(阻塞直到有人收)
value := <-ch // 接收(阻塞直到有人发)
fmt.Println(value) // 输出:42
- 自动同步:发送/接收天然阻塞,无需手动加锁。
- 原子操作:值传递是原子的,不怕中间被插队!
💡 生活类比:Channel 就像快递柜——你把包裹(数据)放进去,对方扫码取走,全程不用见面,也不用争抢。
🃏 第三张:方向限定 —— 单向 Channel
“只许进,不许出;只许收,不许发!”
func produce(out chan<- int) { out <- 1 } // 只能发
func consume(in <-chan int) { fmt.Println(<-in) } // 只能收
- 编译器强制检查:想反着用?直接报错!
- 代码更清晰:一看参数就知道这个函数是“生产者”还是“消费者”。
✅ 最佳实践:函数签名里尽量用单向 channel,让接口意图一目了然!
🃏 第四张:多路复用 —— select
“多个 channel 同时监听?交给 select!”
select {
case msg := <-ch1:
fmt.Println("收到 ch1:", msg)
case <-time.After(1 * time.Second):
fmt.Println("超时啦!")
case <-quit:
return // 优雅退出
}
- 随机选择:多个 case 就绪时,随机选一个,避免饿死。
- 配合
default:实现非阻塞读取!
select {
case v := <-ch:
fmt.Println("有数据:", v)
default:
fmt.Println("没数据,继续干别的")
}
💡 实战技巧:用
quit <-struct{}{}+close(quit)实现“一键关停所有 worker”!
🃏 第五张:缓冲区 —— Buffered Channel
“先存着,别急着发!”
ch := make(chan int, 3)
ch <- 1 // 不阻塞
ch <- 2
ch <- 3
// ch <- 4 // 阻塞!缓冲区满了
适用场景:
- 流量突发(比如秒杀)
- 生产快、消费慢的场景
- 批处理优化
⚠️ 警告:别滥用大缓冲!那只是掩盖设计问题,不是解决它。
批处理小实战:
func batchProcessor(input <-chan int, batchSize int) <-chan []int {
out := make(chan []int)
go func() {
defer close(out)
batch := make([]int, 0, batchSize)
for v := range input {
batch = append(batch, v)
if len(batch) == batchSize {
out <- batch
batch = make([]int, 0, batchSize)
}
}
if len(batch) > 0 {
out <- batch
}
}()
return out
}
🃏 第六张:优雅关闭 —— Close Channel
“发完就关门,别让人乱塞!”
close(ch)
v, ok := <-ch // ok == false 表示已关闭
- 只由发送方关闭!接收方关?等着 panic 吧。
- 用
range自动处理关闭:
for v := range ch {
fmt.Println(v)
} // ch 关闭后自动退出
✅ 黄金法则:谁开 channel,谁负责 close(通常是最后一个 sender)。
🃏 第七张:终极组合 —— 完整 Pipeline 实战
把前面六张卡组合起来,打造一个可取消、可扩展、无泄漏的并发系统!
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
for w := 1; w <= 3; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
processJobs(ctx, id, jobs, results)
}(w)
}
// 发任务
go func() {
for j := 1; j <= 9; j++ { jobs <- j }
close(jobs)
}()
// 收结果
go func() { wg.Wait(); close(results) }()
total := 0
for r := range results {
total += r
fmt.Printf("Got: %d\n", r)
}
fmt.Println("Total:", total)
}
func processJobs(ctx context.Context, id int, jobs <-chan int, results chan<- int) {
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok { return }
time.Sleep(100 * time.Millisecond) // 模拟耗时
select {
case results <- job * 2:
case <-ctx.Done():
return
}
}
}
}
✅ 包含:
- Worker Pool
- Context 取消
- WaitGroup 同步
- Channel 正确关闭
- 非阻塞结果发送
🎯 总结:Go 并发的三大心法
- 通信代替共享:用 channel 传数据,别抢变量。
- 明确职责:谁发、谁收、谁关,清清楚楚。
- 防泄漏、可取消:每个 goroutine 都要有“逃生通道”。
Go 的并发不是“更强大”,而是“更简单”。它逼你写出结构清晰、不易出错的代码——这才是真正的工程之美。
📌 小贴士(来自血泪经验)
- 有时候
sync.Mutex比 channel 更合适(比如保护一个 map)。 - 用
context控制生命周期,别让 goroutine 成“僵尸”。 - 用
go vet和go run -race检查竞态条件! - 记住:可读性 > 聪明技巧。
