使用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 }
关键修改点说明
- 修正WaitGroup使用:在循环启动goroutine之前,一次性调用
wg.Add(len(files.filenames)),确保计数准确;每个goroutine里用defer wg.Done(),保证无论如何都会减少计数。 - 移除错误的done通道逻辑:改用单独的goroutine等待所有任务完成后关闭ch通道,这样主goroutine可以通过
for range ch遍历所有结果,直到通道关闭。 - 添加文件关闭操作:在
checkFile里添加defer file.Close(),避免文件资源泄漏。 - 简化goroutine参数:去掉不必要的
mu、i、filenamesSize等参数,减少复杂度。
内容的提问来源于stack exchange,提问作者Dboi
相关产品推荐
相关产品推荐

