
go 程序中启动 goroutine 后,主协程不会自动等待其执行完毕;若未显式同步(如使用 sync.waitgroup 或 channel),可能在 goroutine 修改共享变量前就已打印结果,导致看到空切片等“无副作用”现象。
在 Go 中,go fill(...) 语句仅表示“异步启动”该函数,而非“阻塞等待完成”。因此,fmt.Println(b1, b2) 很可能在两个 Goroutine 还未开始执行、或仅执行了部分循环时就被调用——此时 b1 和 b2 仍是初始的 nil 切片(打印为 []),造成“未生效”的错觉。
要确保主协程等待 Goroutine 完成,最常用且推荐的方式是使用 sync.WaitGroup:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var (
b1 []float64
b2 []float64
)
func main() {
// 初始化随机数种子(否则 rand.Float64() 每次运行结果相同)
rand.Seed(time.Now().UnixNano())
var wg sync.WaitGroup
wg.Add(2) // 声明将等待 2 个任务
go fill(&b1, 10, &wg)
go fill(&b2, 10, &wg)
wg.Wait() // 阻塞直到所有 Done() 被调用
fmt.Println("b1:", b1)
fmt.Println("b2:", b2)
}
func fill(a *[]float64, n int, wg *sync.WaitGroup) {
defer wg.Done() // 确保无论何种路径退出都计数减一
for i := 0; i < n; i++ {
*a = append(*a, rand.Float64()*100)
}
}✅ 关键要点:
- wg.Add(n) 必须在 go 语句之前调用,避免竞态(如 Goroutine 先执行完 Done() 导致 Wait 提前返回);
- wg.Done() 推荐用 defer 保证执行,尤其当函数内含错误分支或 panic 时;
- wg.Wait() 是同步点,会阻塞当前 Goroutine(此处是 main),直到计数归零。
⚠️ 注意事项:
- 原代码缺失 import "math/rand" 和随机数种子初始化,会导致每次运行输出相同浮点数(虽不影响同步逻辑,但影响结果可观察性);
- 直接操作全局变量 b1/b2 并非最佳实践:它引入隐式依赖和潜在竞态(尽管本例中无并发读写冲突,但可维护性差)。更符合 Go 风格的做法是让 fill 返回新切片,由调用方赋值:
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)
// b2 = fill(b2, 10)这种“值传递 + 返回新切片”的模式更安全、可测试、且与 Go 内置 append 行为一致,也避免了指针参数带来的副作用不确定性。
总结:Goroutine 的本质是并发不等于同步。要观测其对共享状态的修改,必须主动协调执行时序——sync.WaitGroup 是最直观可靠的入门工具,而拥抱不可变/纯函数式风格(如返回新值而非修改原值)则能从根本上减少同步负担与潜在 bug。










