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

使用WaitGroup的Go程序陷入死锁问题排查求助

排查Go程序死锁问题及修复方案

我来帮你梳理下这段代码里导致死锁的核心问题,咱们一步步拆解:

核心问题分析

1. WaitGroup 计数逻辑混乱

  • 你在main开头先调用了wg.Add(1),但这个额外的计数没有对应的wg.Done()来抵消,除非done通道触发,但后面的done逻辑本身就有问题。
  • 更关键的是:你在goroutine内部调用wg.Add(1),这是非常危险的——goroutine的启动是异步的,可能wg.Wait()已经开始等待了,部分goroutine还没执行到wg.Add(1),导致WaitGroup的计数始终不对,最终阻塞等待。

2. Done 通道的触发逻辑完全错误

  • 你用if i == *filenamesSize来判断所有goroutine是否完成,这逻辑根本不成立:i是循环的索引(比如0、1、2...),而*filenamesSize在每个goroutine里递减,比如初始是3,第一个goroutine把它减到2,i=0不等于2;第二个减到1,i=1等于1,这时候就触发done,但第三个goroutine还在运行——此时main里的select收到done就执行一次wg.Done(),然后开始wg.Wait(),但第三个goroutine的wg.Done()还没执行,导致WaitGroup一直等待,死锁。
  • 另外,多个goroutine可能同时满足这个错误的判断条件,会多次往done通道发消息甚至重复close(done),这会直接引发panic。

3. Select 语句只处理一次事件

main里的select只会执行一次:要么读取一个ch的消息,要么收到done的信号,然后就break了。但ch是带缓冲的,其他goroutine还在往ch里发送数据,同时WaitGroup的计数还没清零,wg.Wait()就会一直阻塞,造成死锁。


修复后的完整代码

我把这些问题都修复了,你可以参考:

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
    "path/filepath"
    "strings"
    "sync"
)

type Files struct {
    filenames []string
}

type Orders struct {
    ID []string
}

var ordersFilename string = "orders.csv"

func main() {
    var (
        ordersFile *os.File
        files Files
        orders Orders
        err error
    )
    wg := &sync.WaitGroup{}
    
    if ordersFile, err = os.Open(ordersFilename); err != nil {
        log.Fatalln("Could not open file: " + ordersFilename)
    }
    orders = getOrderIDs(ordersFile)
    files.filenames = getCSVsFromCurrentDir()
    filenamesSize := len(files.filenames)
    ch := make(chan map[string][]string, filenamesSize)

    // 提前给WaitGroup加好所有goroutine的计数,避免异步问题
    wg.Add(filenamesSize)
    for _, filename := range files.filenames {
        go func(currentFilename string, ch chan<- map[string][]string, orders Orders, wg *sync.WaitGroup) {
            defer wg.Done() // 确保goroutine退出时一定会调用Done
            checkFile(currentFilename, orders, ch)
        }(filename, ch, orders, wg)
    }

    // 启动一个单独的goroutine,等所有任务完成后关闭通道
    go func() {
        wg.Wait()
        close(ch)
    }()

    // 遍历通道里的所有结果,直到通道关闭
    for result := range ch {
        fmt.Printf("%+v\n", result)
    }

    fmt.Println("所有文件比对完成")
}

// getCSVsFromCurrentDir returns a string slice
// with the filenames of csv files inside the
// current directory that are not "orders.csv"
func getCSVsFromCurrentDir() []string {
    var filenames []string
    err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
        if path != "." && strings.HasSuffix(path, ".csv") && path != ordersFilename {
            filenames = append(filenames, path)
        }
        return nil
    })
    if err != nil {
        log.Fatalln("Could not read file names in current dir")
    }
    return filenames
}

// getOrderIDs returns an Orders struct filled
// with order IDs retrieved from the file
func getOrderIDs(file *os.File) Orders {
    var (
        orders Orders
        err error
        fileContent string
    )
    reader := bufio.NewReader(file)
    if fileContent, err = readLine(reader); err != nil {
        log.Fatalln("Could not read file: " + ordersFilename)
    }
    for err == nil {
        orders.ID = append(orders.ID, fileContent)
        fileContent, err = readLine(reader)
    }
    return orders
}

func checkFile(filename string, orders Orders, ch chan<- map[string][]string) {
    var (
        err error
        file *os.File
        fileContent string
        orderFilesMap map[string][]string
    )
    orderFilesMap = make(map[string][]string)
    if file, err = os.Open(filename); err != nil {
        log.Fatalln("Could not read file: " + filename)
    }
    defer file.Close() // 记得关闭文件,避免资源泄漏
    reader := bufio.NewReader(file)
    if fileContent, err = readLine(reader); err != nil {
        log.Fatalln("Could not read file: " + filename)
    }
    for err == nil {
        if containedInSlice(fileContent, orders.ID) && !containedInSlice(fileContent, orderFilesMap[filename]) {
            orderFilesMap[filename] = append(orderFilesMap[filename], fileContent)
            // fmt.Println("Found: ", fileContent, " in ", filename)
        } else {
            // fmt.Printf("Could not find: '%s' in '%s'\n", fileContent, filename)
        }
        fileContent, err = readLine(reader)
    }
    ch <- orderFilesMap
}

// containedInSlice returns true or false
// based on whether the string is contained
// in the slice
func containedInSlice(str string, slice []string) bool {
    for _, ID := range slice {
        if ID == str {
            return true
        }
    }
    return false
}

// readLine returns a line from the passed reader
func readLine(r *bufio.Reader) (string, error) {
    var (
        isPrefix bool = true
        err error = nil
        line, ln []byte
    )
    for isPrefix && err == nil {
        line, isPrefix, err = r.ReadLine()
        ln = append(ln, line...)
    }
    return string(ln), err
}

关键修改点说明

  1. 修正WaitGroup使用:在循环启动goroutine之前,一次性调用wg.Add(len(files.filenames)),确保计数准确;每个goroutine里用defer wg.Done(),保证无论如何都会减少计数。
  2. 移除错误的done通道逻辑:改用单独的goroutine等待所有任务完成后关闭ch通道,这样主goroutine可以通过for range ch遍历所有结果,直到通道关闭。
  3. 添加文件关闭操作:在checkFile里添加defer file.Close(),避免文件资源泄漏。
  4. 简化goroutine参数:去掉不必要的mu、i、filenamesSize等参数,减少复杂度。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 08:01:45