Go语言中goroutine执行同步的正确实践

2次阅读

Go语言中goroutine执行同步的正确实践

go程序中启动goroutine后,主协程不会自动等待其完成;若未显式同步(如使用sync.waitgroup或channel),主函数可能在goroutine执行前就已退出,导致预期的副作用(如切片填充)未生效。

在Go中,go关键字启动的是非阻塞异步协程:主goroutine(即main函数)会立即继续执行后续语句,而不会暂停等待新协程结束。这正是原代码输出空切片的根本原因——fmt.Println(b1, b2)在fill函数尚未运行或仅部分运行时就被执行了,随后程序可能直接退出(即使有fmt.Scanln,也属于不可靠的“碰运气”式等待)。

要确保主协程等待所有工作goroutine完成,推荐使用 sync.WaitGroup —— 它专为这类“等待一组goroutine结束”的场景设计。关键步骤有三:

  • 创建 WaitGroup 实例;
  • 启动goroutine前调用 wg.Add(n) 声明需等待的goroutine数量;
  • 每个goroutine结尾调用 wg.Done() 表明自身已完成;
  • 主goroutine中调用 wg.Wait() 阻塞直到所有计数归零。

以下是修正后的完整可运行代码:

package main  import (     "fmt"     "math/rand"     "sync"     "time" )  var (     b1 []float64     b2 []float64 )  func main() {     // 初始化随机数种子(否则每次运行结果相同)     rand.Seed(time.Now().unixNano())      var wg sync.WaitGroup     wg.Add(2) // 声明将等待2个goroutine      go fill(&b1, 10, &wg)     go fill(&b2, 10, &wg)      wg.Wait() // 主goroutine在此阻塞,直至两个fill完成      fmt.Println("b1:", b1)     fmt.Println("b2:", b2) }  func fill(a *[]float64, n int, wg *sync.WaitGroup) {     defer wg.Done() // 确保无论何种路径退出,都调用Done()     for i := 0; i < n; i++ {         *a = append(*a, rand.Float64()*100)     } }

⚠️ 注意事项:

立即学习go语言免费学习笔记(深入)”;

  • 必须导入 "math/rand" 和 "time":原代码遗漏了rand.Float64()所需的包,且未设置随机种子,会导致重复结果;
  • wg.Add() 必须在 go 语句之前调用:避免竞态(如Add和Done顺序错乱);
  • 建议用 defer wg.Done():比裸写wg.Done()更安全,能保证即使函数中途panic也执行清理;
  • 避免依赖 fmt.Scanln 等I/O作为同步手段:它不可靠、不直观,且掩盖了并发控制的本质问题。

? 进阶建议:从设计角度看,修改传入切片指针的方式虽可行,但更符合Go惯用法的是返回新切片(类似append语义),既避免共享状态,又提升函数纯度与可测试性:

func fill(src []float64, n int) []float64 {     for i := 0; i < n; i++ {         src = append(src, rand.Float64()*100)     }     return src } // 调用方式:b1 = fill(b1, 10)

这种值传递风格更安全、更易推理,也是Go官方Code Review Comments明确倡导的最佳实践。

text=ZqhQzanResources