服务公告

服务公告 > 综合新闻 > Go:并发编程

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: slow

select会随机选择就绪的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:性能优化