为什么人们对此不满?Golang相关讨论
为什么人们对此不满?Golang相关讨论 大家好,我最近开始学习 Go 语言。在解决一些问题时,我写出了以下代码:
package main
import (
"fmt"
)
func main() {
s := []int{1,2,3,4,5}
count := len(s)
finishSignal := make (chan bool, count)
for i := range s {
t := i
go func () {
s[t] += 1
finishSignal <- true
} ()
}
for _ = range s {
<- finishSignal
}
fmt.Print(s)
}
这段示例代码使用了一种我认为在 Go 中相当常见的“分散-收集”模式:拆分工作、创建工作协程并等待结果。令我惊讶的是,许多人说这段代码很糟糕,并建议我应该使用标准库中预定义的实用工具,确切地说是 WaitGroup。
我的问题是:“这段代码有什么问题?” 它真的很糟糕吗?
更多关于为什么人们对此不满?Golang相关讨论的实战教程也可以访问 https://www.itying.com/category-94-b0.html
我来告诉你为什么我认为这不是你想要的行为。以这种方式使用通道,除非你仔细思考,否则并不清楚你在做什么,但 WaitGroup 的使用方式很明确,并且使代码更易于阅读。 https://gobyexample.com/waitgroups
更多关于为什么人们对此不满?Golang相关讨论的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
我对Go语言还非常陌生,但根据我的理解,通道“可以”用作同步原语,但它们的主要目的是在goroutine之间共享信息。
WaitGroup 的设计初衷是等待一组goroutine完成。
我只有在goroutine返回某些结果时才会使用通道。
func main() {
fmt.Println("hello world")
}
如果存在惯用的方法,使用它将减少未来需要阅读你代码的开发者的开销。在你的例子中,我不得不问自己“好吧,这里的意图是什么?”。如果你使用了 sync.WaitGroup,我本可以更容易地理解你的意图。
假设你正在代码审查我的提交,它看起来像这样:
package main
import (
"fmt"
)
// 一个刻意构造的例子...
func main() {
for i := 1; i <= 10; i++ {
DoIf(i%2 == 0, func() {
fmt.Println(i, "is even")
})
}
}
// DoIf 如果 value 为 true 则运行 f。
func DoIf(value bool, f func()) {
if value {
f()
}
}
你需要做更多的解析才能理解 DoIf 在做什么。它也显得更冗长(就像你的示例代码一样)。如果我重构为以下形式,会更容易理解,对吗?
if i%2 == 0 {
fmt.Println(i, "is even")
}
对于其他维护者来说,符合惯例的代码总是更好。sync.WaitGroup 是等待任务完成的惯用方式,因此你应该使用它。至少这是我的看法。
这段代码确实存在几个问题,虽然功能上能完成工作,但在实际生产环境中是不推荐的。主要问题如下:
1. 数据竞争(Data Race)
你的代码存在潜在的数据竞争问题。多个goroutine同时修改切片s的不同元素,虽然看起来每个goroutine访问不同的索引,但Go的内存模型不能保证这种并发访问的安全性。
// 有数据竞争风险
go func() {
s[t] += 1 // 多个goroutine并发写入切片
finishSignal <- true
}()
2. 通道缓冲大小不必要
你创建了一个缓冲通道,大小等于切片长度,这实际上抵消了使用goroutine的并发优势:
finishSignal := make(chan bool, count) // 缓冲大小等于任务数
这意味着所有goroutine会立即完成发送,然后主goroutine再接收,失去了真正的并发等待意义。
3. 闭包捕获问题
虽然你使用了t := i来避免闭包捕获循环变量的问题,但代码仍然不够清晰。
使用WaitGroup的改进版本:
package main
import (
"fmt"
"sync"
)
func main() {
s := []int{1, 2, 3, 4, 5}
var wg sync.WaitGroup
for i := range s {
wg.Add(1)
go func(idx int) {
defer wg.Done()
s[idx] += 1
}(i)
}
wg.Wait()
fmt.Print(s) // 输出: [2 3 4 5 6]
}
更安全的并发版本(使用互斥锁):
package main
import (
"fmt"
"sync"
)
func main() {
s := []int{1, 2, 3, 4, 5}
var wg sync.WaitGroup
var mu sync.Mutex
for i := range s {
wg.Add(1)
go func(idx int) {
defer wg.Done()
mu.Lock()
s[idx] += 1
mu.Unlock()
}(i)
}
wg.Wait()
fmt.Print(s)
}
使用Worker Pool模式(更高效):
package main
import (
"fmt"
"sync"
)
func main() {
s := []int{1, 2, 3, 4, 5}
var wg sync.WaitGroup
jobs := make(chan int, len(s))
// 启动固定数量的worker
for w := 0; w < 3; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for idx := range jobs {
s[idx] += 1
}
}()
}
// 发送任务
for i := range s {
jobs <- i
}
close(jobs)
wg.Wait()
fmt.Print(s)
}
为什么人们推荐WaitGroup:
- 更清晰的意图表达:
WaitGroup明确表达了"等待一组goroutine完成"的意图 - 更少的样板代码:不需要手动管理通道的发送/接收
- 更好的性能:避免不必要的通道缓冲和上下文切换
- 标准库支持:是Go并发模式的标准做法
你的原始代码虽然能运行,但违反了Go的并发最佳实践。在Go中,清晰性和安全性比小聪明更重要。

