方法1
代码
package main
import (
"fmt"
"sync"
"time"
)
func main() {
allChan := make(chan interface{}, 3)
var sendWg, recvWg sync.WaitGroup // 分别同步发送和接收
// 发送goroutine
sendWg.Add(1)
go func() {
defer sendWg.Done()
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond * 10)
allChan <- i
fmt.Printf("发送: %d\n", i)
}
close(allChan) // 发送完成后关闭channel
}()
// 接收goroutine
recvWg.Add(1)
go func() {
defer recvWg.Done()
for item := range allChan {
time.Sleep(time.Millisecond * 20) // 模拟处理耗时
fmt.Printf("接收: %v\n", item)
}
}()
// 先等待发送完成,再等待接收完成
sendWg.Wait()
recvWg.Wait()
fmt.Println("所有操作完成")
}
运行结果:
标准输出:发送: 0
发送: 1
接收: 0
发送: 2
发送: 3
接收: 1
发送: 4
发送: 5
接收: 2
发送: 6
接收: 3
发送: 7
接收: 4
发送: 8
接收: 5
发送: 9
接收: 6
接收: 7
接收: 8
接收: 9
所有操作完成
分析
优化说明
- 双重同步:
- 使用两个WaitGroup(sendWg和recvWg),分别等待发送和接收 goroutine 完成。
- 先通过sendWg.Wait()确保所有数据发送完毕并关闭 channel;
- 再通过recvWg.Wait()确保接收 goroutine 处理完所有数据。
- channel 关闭逻辑:
- 发送完成后关闭 channel 是关键,这会让接收 goroutine 的range循环在取完数据后自动退出,避免接收端阻塞。
- 适用场景:
- 这种 “发送 + 接收都异步” 的模式适合需要主程序同时处理其他逻辑的场景(例如同时监控多个任务),但必须做好同步,否则会出现数据丢失。
方法2
代码
package main
import (
"fmt"
"time"
)
func main() {
// 1. 数据通道:传递实际数据
dataChan := make(chan interface{}, 3)
// 2. 发送完成信号通道:发送方结束后通知
sendDone := make(chan struct{})
// 3. 接收完成信号通道:接收方结束后通知
recvDone := make(chan struct{})
// 发送goroutine
go func() {
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond * 10)
dataChan <- i
fmt.Printf("发送: %d\n", i)
}
close(dataChan) // 发送完毕,关闭数据通道(通知接收方结束)
sendDone <- struct{}{} // 发送完成信号
}()
// 接收goroutine
go func() {
for item := range dataChan { // 当dataChan关闭且数据取完后,循环自动退出
time.Sleep(time.Millisecond * 20)
fmt.Printf("接收: %v\n", item)
}
recvDone <- struct{}{} // 接收完成信号
}()
// 主程序等待:先等发送完成,再等接收完成
<-sendDone // 阻塞等待发送完成
<-recvDone // 阻塞等待接收完成
fmt.Println("所有操作完成")
}
运行结果
标准输出:发送: 0
发送: 1
接收: 0
发送: 2
发送: 3
接收: 1
发送: 4
发送: 5
接收: 2
发送: 6
接收: 3
发送: 7
接收: 4
发送: 8
接收: 5
发送: 9
接收: 6
接收: 7
接收: 8
接收: 9
所有操作完成
那种更好呢
一、效率对比
sync.WaitGroup 略占优势
- sync.WaitGroup 是 Go 标准库专门为 “等待一组 goroutine 完成” 设计的同步原语,底层通过原子操作(计数器加减)实现,几乎没有额外开销,执行效率极高。
- 它的逻辑简单:Add 增加计数、Done 减少计数、Wait 阻塞直到计数归零,整个过程不涉及复杂的 runtime 调度交互。
多通道同步的开销稍高 - 通道同步依赖于 channel 的发送 / 接收操作,这些操作需要与 Go runtime 的调度器交互(如检查通道状态、唤醒阻塞的 goroutine 等),理论上比原子操作多一些微小的开销。
- 额外的信号通道(如 sendDone、recvDone)会占用少量内存(每个通道需要维护内部数据结构),但在实际应用中影响可忽略。
二、实用性与适用场景
sync.WaitGroup 更适合大多数场景
- 代码简洁:对于 “等待多个 goroutine 完成” 的场景,WaitGroup 的语义更直观(Add/Done/Wait 直接对应 “注册 - 完成 - 等待” 逻辑),可读性更高。
- 扩展性好:如果需要等待多个发送者或接收者(如 10 个发送 goroutine),WaitGroup 只需一次 Add(10) 即可,无需额外定义多个信号通道。
- 通用性强:是 Go 社区的 “标准做法”,团队协作时更容易被理解。
多通道同步适合特定场景 - 符合 Go 哲学:更贴合 “用通信实现共享内存” 的 Go 设计思想,通过 channel 传递信号比直接操作计数器更 “Go 式”。
- 灵活扩展:如果需要更复杂的同步逻辑(如 “先等待 A 完成,再启动 B,最后等待 B 完成”),通道的阻塞特性可以天然实现流程控制,而 WaitGroup 可能需要配合额外逻辑。
- 无依赖:不依赖 sync 包,纯靠语言原生特性实现,适合理解 channel 机制的场景。
三、总结建议
优先选 sync.WaitGroup:在大多数业务代码中,它更简洁、高效、易维护,是同步 goroutine 的 “标准答案”。
选多通道同步:当你需要强调 “通信优先” 的设计,或同步逻辑较复杂(需要通过通道传递更多状态)时,它是更优雅的选择。
本质区别:
WaitGroup 是 “共享状态” 式同步(通过计数器),多通道是 “通信” 式同步(通过信号传递)。两者效率差异微小,选择时主要看代码可读性和场景适配性。