
本文讲解如何在 go 中使用 goroutine 安全修改结构体字段,并通过 sync.waitgroup 正确等待所有并发操作完成,避免因竞态导致的值未更新问题。
本文讲解如何在 go 中使用 goroutine 安全修改结构体字段,并通过 sync.waitgroup 正确等待所有并发操作完成,避免因竞态导致的值未更新问题。
在 Go 中,直接在循环中启动 goroutine 并传入结构体值(如 a[i].cube_val())会导致两个关键问题:值拷贝语义和缺乏同步机制。
首先,a[i] 是一个结构体值,go a[i].cube_val() 实际上传递的是该结构体的副本(即使方法是为指针接收者定义的),因此 cube_val() 内部对 d.val 的修改仅作用于副本,不影响原始切片中的元素。其次,主 goroutine 不会自动等待子 goroutine 执行完毕——fmt.Println(a) 极大概率在所有 cube_val() 调用完成前就执行了,导致输出未修改的原始值。
✅ 正确做法是:显式传递结构体指针,并使用 sync.WaitGroup 同步生命周期。以下是修复后的完整示例:
package main
import (
"fmt"
"sync"
)
type num struct {
val int
}
func (d *num) cube_val() {
fmt.Printf("cubing %d → ", d.val)
d.val = d.val * d.val * d.val
fmt.Println(d.val)
}
func main() {
a := []num{num{1}, num{3}, num{2}, num{5}, num{4}}
var wg sync.WaitGroup
for i := range a {
wg.Add(1)
// 传入 &a[i] 的指针,确保修改作用于原切片元素
go func(d *num) {
defer wg.Done() // 确保 Done() 在 goroutine 结束时调用
d.cube_val()
}(&a[i])
}
wg.Wait() // 阻塞直到所有 goroutine 调用 Done()
fmt.Println("Final slice:")
fmt.Println(a) // 输出: [{1} {27} {8} {125} {64}]
}
⚠️ 注意事项:
-
避免循环变量捕获陷阱:若写成
go func() { ... }(); wg.Done()并在循环内直接引用i或a[i],由于闭包捕获的是变量地址,所有 goroutine 可能共享同一个i的最终值(如i == 5),导致 panic 或错误索引。因此必须将&a[i]作为参数显式传入。 -
WaitGroup 必须在 goroutine 启动前调用
Add(),且Done()应在 goroutine 内部调用(推荐用defer保证执行)。 - 若结构体较大或需频繁并发读写,还需考虑
sync.Mutex等保护机制;但本例中每个 goroutine 操作独立切片元素,无数据竞争,故无需额外锁。
总结:Go 的并发模型强调显式同步。修改共享状态必须传递指针 + 使用 sync.WaitGroup 等原语协调执行顺序——这是写出可靠、可预测并发代码的基础实践。










