服务公告
Go:并发编程
发布时间:2026-04-24 10:01
一、前言
搞过的人都知道,Go最香的就是并发模型,但你要是直接拿channel和goroutine裸写,八成会写出死锁或者数据竞争。这篇不废话,直接上生产级并发代码的套路,怎么写、怎么测、怎么避开那些坑。
二、操作步骤
步骤1:先搞懂goroutine怎么spawn
package main
import (
"fmt"
"time"
)
func worker(id int) {
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
for i := 1; i <= 3; i++ {
go worker(i) // 启动3个goroutine
}
time.Sleep(2 * time.Second) // 等所有worker完成
}预期输出:
Worker 2 started
Worker 1 started
Worker 3 started
Worker 1 done
Worker 2 done
Worker 3 done顺序不固定,goroutine调度是抢占式的。但注意,main退出了这些worker也就没了。
步骤2:用WaitGroup管控生命周期
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 关键:完成后通知
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg) // 注意传递指针
}
wg.Wait() // 阻塞直到计数器归零
fmt.Println("All workers finished")
}预期输出:
Worker 2 started
Worker 4 started
Worker 1 started
Worker 3 started
Worker 5 started
Worker 1 done
Worker 2 done
Worker 3 done
Worker 4 done
Worker 5 done
All workers finished这才是正确的姿势,别再用time.Sleep硬等了。
步骤3:channel的正确打开方式
package main
import "fmt"
func producer(ch chan<- int) { // 只发送
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch) // 发送完必须关闭
}
func consumer(ch <-chan int, done chan<- bool) { // 只接收
for v := range ch {
fmt.Printf("Received: %d\n", v)
}
done <- true
}
func main() {
ch := make(chan int, 10) // 带缓冲,容量10
done := make(chan bool)
go producer(ch)
go consumer(ch, done)
<-done // 等待消费者完成
fmt.Println("Main exit")
}预期输出:
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
Main exit缓冲channel能缓解生产者和消费者的速度差,但别把缓冲当万能药。
步骤4:select多路复用的坑
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(500 * time.Millisecond)
ch1 <- "slow"
}()
go func() {
time.Sleep(100 * time.Millisecond)
ch2 <- "fast"
}()
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Printf("ch1: %s\n", msg)
case msg := <-ch2:
fmt.Printf("ch2: %s\n", msg)
case <-time.After(time.Second):
fmt.Println("timeout")
}
}
}预期输出:
ch2: fast
ch1: slowselect会随机选择就绪的channel,别假设顺序。超时机制一定要加,不然goroutine泄漏了都不知道。
步骤5:mutex保护共享变量
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
val int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.val++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.val
}
func main() {
var wg sync.WaitGroup
c := &Counter{}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc()
}()
}
wg.Wait()
fmt.Printf("Final value: %d\n", c.Value())
}预期输出:
Final value: 1000
如果不用mutex,这个值基本不会是1000。sync.Mutex是Go里最常用的同步手段。
步骤6:context取消长时间操作
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d cancelled: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d working...\n", id)
time.Sleep(200 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
<-ctx.Done()
fmt.Println("Main exit, context timed out")
}预期输出:
Worker 2 working...
Worker 1 working...
Worker 3 working...
Worker 1 working...
Worker 2 working...
Worker 3 working...
Worker 1 cancelled: context deadline exceeded
Worker 2 cancelled: context deadline exceeded
Worker 3 cancelled: context deadline exceeded
Main exit, context timed out
context是控制 goroutine 生命周期的标准方式,生产环境必用。
三、常见问题FAQ
Q:channel突然panic了,怎么回事?
A:向已关闭的channel发送会panic。从已关闭channel接收不会panic,但会返回零值。想安全关闭channel?记住这个口诀:有且仅有发送方close,别在接收方close,别重复close。
Q:goroutine泄漏怎么排查?
A:用pprof的goroutine profile,看哪个协程数量不正常。最常见原因:select没加default分支但case永远不满足,或者channel没人写但也没人关。生产环境建议定期dump goroutine profile基线。
Q:sync.Mutex和atomic性能差多少?
A:atomic适合简单计数器这种场景,开销大概只有Mutex的1/10。但复杂逻辑别硬用atomic,容易写错。业务代码优先保证正确,热点场景再优化。
Q:race detector怎么用?
A:go test -race,你的单元测试加上这个flag跑一遍。生产二进制也加-race编译,会显著增加内存开销但能发现数据竞争。别在正式环境开,但CI必须跑。
四、总结
Go并发核心就三板斧:goroutine spawn任务,channel传递数据,sync原语保护共享状态。记住几点:
- goroutine要成对出现:spawn了就得有人等它结束,用WaitGroup或者context管
- channel遵循谁写谁关原则,关闭前确保没人再写
- 共享变量必须加锁,atomic只能用于无逻辑的简单操作
- 生产代码必须跑-race检测,数据竞争是隐藏最深的地雷
延伸阅读:官方文档Concurrency is Not Parallelism必读,想深入可以看The Go Memory Model,理解了happens-before才能写出正确的并发代码。实战中多积累踩坑经验,书上的规则都是血泪换来的。
相关推荐
上一篇: Trae:常见问题
下一篇: Memcached:性能优化