为什么人们对此不满?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

4 回复

我来告诉你为什么我认为这不是你想要的行为。以这种方式使用通道,除非你仔细思考,否则并不清楚你在做什么,但 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:

  1. 更清晰的意图表达WaitGroup明确表达了"等待一组goroutine完成"的意图
  2. 更少的样板代码:不需要手动管理通道的发送/接收
  3. 更好的性能:避免不必要的通道缓冲和上下文切换
  4. 标准库支持:是Go并发模式的标准做法

你的原始代码虽然能运行,但违反了Go的并发最佳实践。在Go中,清晰性和安全性比小聪明更重要。

回到顶部