You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go Channel偶发无法接收最后一个值的问题排查

Go Channel 计数偶发不足且添加WaitGroup死锁问题分析

问题描述

我正在学习Go Channel,编写了测试代码:创建10个goroutine,每个向channel发送1000个数值1;另一个goroutine从channel接收值并累加到计数器中,用WaitGroup确保goroutine执行完毕后打印结果。但运行后计数器有时无法达到预期的10000,偶发为9999。尝试为接收goroutine添加另一个WaitGroup却引发死锁,请问代码存在什么问题?

测试代码

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func main() {
    var counter int

    fmt.Println("\nWithout Channels -------")
    for i := 0; i < 10; i++ {
        go func() {
            for j := 0; j < 1000; j++ {
                // to simulate race condition
                time.Sleep(time.Duration(1))
                counter++
            }
        }()
    }

    runtime.Gosched()

    fmt.Println("Expected counter: 10000, Actual counter:", counter)

    fmt.Println("\nWith Channels -------")
    for i := 0; i < 100; i++ {
        WithChannels()
    }
}

func WithChannels() {
    var counter int
    ch := make(chan int)
    var wg sync.WaitGroup
    // var wg2 sync.WaitGroup

    wg.Add(10)
    for i := 0; i < 10; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                time.Sleep(time.Duration(1))
                ch <- 1
            }
        }()
    }

    // wg2.Add(1)
    go func() {
        for {
            increment, ok := <-ch
            if !ok {
                // channel closed, break
                break
            }
            counter += increment
        }
        // wg2.Done()
    }()

    wg.Wait()
    // wg2.Wait()
    // time.Sleep(time.Duration(1) * time.Second)
    fmt.Println("Expected counter: 10000, Actual counter:", counter)
    close(ch)
}

运行输出

Without Channels -------
Expected counter: 10000, Actual counter: 139

With Channels -------
Expected counter: 10000, Actual counter: 9999
Expected counter: 10000, Actual counter: 10000
Expected counter: 10000, Actual counter: 9999
Expected counter: 10000, Actual counter: 10000
Expected counter: 10000, Actual counter: 10000
Expected counter: 10000, Actual counter: 9999
...

问题根源分析

1. 计数偶发9999的原因

代码在wg.Wait()等待所有发送goroutine完成后,直接打印计数器,再关闭channel。这里存在时序漏洞:当最后一个发送goroutine把值写入channel后,wg.Done()触发wg.Wait()返回,但接收goroutine可能还没来得及读取这个值,此时打印的计数器就会少算未处理的数值,导致偶发9999。

2. 添加wg2引发死锁的原因

如果启用注释中的wg2相关代码,流程会变成:

  • wg.Wait()等待发送goroutine完成
  • 调用wg2.Wait()等待接收goroutine完成
  • 但此时channel未关闭,接收goroutine会一直阻塞在<-ch上,永远无法执行wg2.Done(),主goroutine会无限等待,最终引发死锁。

正确解决方法

调整时序逻辑:等所有发送goroutine完成后,先关闭channel,再等待接收goroutine处理完所有剩余值,最后打印结果。

修改后的WithChannels()函数:

func WithChannels() {
    var counter int
    ch := make(chan int)
    var wg sync.WaitGroup
    var wg2 sync.WaitGroup

    wg.Add(10)
    for i := 0; i < 10; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                time.Sleep(time.Duration(1))
                ch <- 1
            }
        }()
    }

    wg2.Add(1)
    go func() {
        defer wg2.Done()
        // 用range遍历channel,channel关闭后会自动退出循环
        for increment := range ch {
            counter += increment
        }
    }()

    wg.Wait()         // 等待所有发送goroutine完成
    close(ch)         // 关闭channel,通知接收goroutine无新值
    wg2.Wait()        // 等待接收goroutine处理完所有值
    fmt.Println("Expected counter: 10000, Actual counter:", counter)
}

关键修改点

  • 发送完成后先关闭channel:关闭后接收端的range遍历会处理完所有剩余值后自动退出
  • 等待接收goroutine完成后再打印计数器,确保所有值都被累加
  • 用for range替代手动判断channel状态的无限循环,代码更简洁可靠

另外补充:原代码中Without Channels部分的问题是竞态条件,多个goroutine无同步地修改counter,需要用sync.Mutex或原子操作保护变量修改。

内容的提问来源于stack exchange,提问作者razzz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 18:53:20