V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
hsczy
V2EX  ›  Go 编程语言

Golang 新手求助

  •  
  •   hsczy · 2021-02-24 16:29:10 +08:00 · 3984 次点击
    这是一个创建于 1400 天前的主题,其中的信息可能已经有所发展或是发生改变。

    帮朋友做 excel 的简单处理。原本是 python 栈,心想使用省事就像打包成 exe 文件,python 打包的 exe 文件又太大,而且会有各种奇奇怪怪的问题。刚好最近在学 golang,就用 golang 在写代码,不过遇到了 Goroutine 和 Channel 配合使用的问题。

    问题的大致是这样的,先找出所有的符合条件的 Excel 文件,放入一个 chanfileChan中,然后通过读取这个 chan 中的数据,使用 Goroutine 调用utlis.ReadExcelFile的方法将传入的文件进行分析,只对需要的处理的行进行处理,并用一个 chanResultChan去接受,最后再将读取到的结果进行处理。但是对ResultChan进行处理之后就会报死锁的错误,我怎么样都不能定位到原因。请各位好哥哥帮帮我。 代码如下

    main.go

    package main
    
    import (
    	utils "excelmaker/src/Utils"
    	"fmt"
    	"log"
    	"os"
    	"path/filepath"
    	"strings"
    	"sync"
    )
    
    func main() {
    	pwd, _ := os.Getwd()                                         // 找到本地路径
    	filenamePaths, err := filepath.Glob(filepath.Join(pwd, "*")) //获取本地目录下所有文件
    	if err != nil {
    		log.Fatal(err)
    	}
    	fileChan := make(chan string, 5)
    	for _, filePathName := range filenamePaths {
    		fileExt := filepath.Ext(filePathName)
    		fileName := filepath.Base(filePathName)
    		if fileExt == ".xlsx" && strings.Contains(fileName, "工资表") {
    			fmt.Println(filePathName)
    			fileChan <- filePathName
    		}
    	}
    	totalFileNum := len(fileChan)
    	wg := sync.WaitGroup{}
    	wg.Add(totalFileNum)
    	for filePathName := range fileChan {
    		go utils.ReadExcelFile(filePathName, &wg)
    	}
    	go utils.MakeTotalExcel() //会出现死锁
    	wg.Wait()
    
    }
    
    

    Utils/utils.go

    package utils
    
    import (
    	"fmt"
    	"strconv"
    	"strings"
    	"sync"
    
    	"github.com/360EntSecGroup-Skylar/excelize/v2"
    )
    
    type ExcelData struct {
    	Name    string
    	Salar   int
    	Company string
    }
    
    var ResultChan = make(chan *ExcelData, 10)
    
    const SalaryTable = "工资表"
    
    func ReadExcelFile(filePathName string, wg *sync.WaitGroup) {
    	// func ReadExcelFile(filePathName string) {
    	f, err := excelize.OpenFile(filePathName)
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	var company string
    	if strings.Contains(filePathName, "头疗") {
    		company = "头疗店"
    	} else if strings.Contains(filePathName, "城东") {
    		company = "城东店"
    	} else if strings.Contains(filePathName, "熹 SPA") {
    		company = "熹 SPA 店"
    	} else if strings.Contains(filePathName, "置地") {
    		company = "置地店"
    	} else {
    		company = "东方丽景店"
    	}
    	result, _ := f.SearchSheet(SalaryTable, "实发工资", false)
    	salaryColIndex, _, _ := excelize.CellNameToCoordinates(result[0])
    	rows, err := f.Rows(SalaryTable)
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	for rows.Next() {
    		row, err := rows.Columns()
    		if err != nil {
    			fmt.Println(err)
    			return
    		}
    		if len(row) != 0 {
    			var stringNotAnalysisList = []string{"", "姓名", "合计"}
    			if ok, _ := Contain(row[0], stringNotAnalysisList); ok {
    				continue
    			} else {
    				salary, _ := strconv.Atoi(row[salaryColIndex-1])
    				tempData := ExcelData{
    					Name:    row[0],
    					Salar:   salary,
    					Company: company,
    				}
    				ResultChan <- &tempData
    			}
    		}
    	}
    	wg.Done()
    }
    
    func MakeTotalExcel() {
    	resultMap := make(map[string]map[string]int)
    	companyList := []string{"姓名"}
    	for v := range ResultChan {
    		// fmt.Println(v.Name)
    		if ok, _ := Contain(v.Name, resultMap); !ok {
    			resultMap[v.Name] = make(map[string]int)
    		}
    		resultMap[v.Name][v.Company] = v.Salar
    		if ok, _ := Contain(v.Company, companyList); !ok {
    			companyList = append(companyList, v.Company)
    		}
    	}
    	fmt.Println(companyList)
    	fmt.Println(resultMap)
    }
    
    

    Utils/commont_utils.go

    package utils
    
    import (
    	"errors"
    	"reflect"
    )
    
    // 判断 obj 是否在 target 中,target 支持的类型 arrary,slice,map
    func Contain(obj interface{}, target interface{}) (bool, error) {
    	targetValue := reflect.ValueOf(target)
    	switch reflect.TypeOf(target).Kind() {
    	case reflect.Slice, reflect.Array:
    		for i := 0; i < targetValue.Len(); i++ {
    			if targetValue.Index(i).Interface() == obj {
    				return true, nil
    			}
    		}
    	case reflect.Map:
    		if targetValue.MapIndex(reflect.ValueOf(obj)).IsValid() {
    			return true, nil
    		}
    	}
    
    	return false, errors.New("not in array")
    }
    
    
    37 条回复    2021-03-08 17:02:17 +08:00
    luguhu
        1
    luguhu  
       2021-02-24 16:41:30 +08:00
    因为 channel 没有 Close 吧
    hsczy
        2
    hsczy  
    OP
       2021-02-24 16:44:21 +08:00
    @luguhu 使用 channel 需要 close 么?我看教程上面没有显式关闭的模式。如果要关闭的话 是在 wg.wait()之后关闭么?
    darksword21
        3
    darksword21  
       2021-02-24 16:45:50 +08:00
    我觉得还是 python 好写一些。。
    hsczy
        4
    hsczy  
    OP
       2021-02-24 16:47:08 +08:00
    @darksword21 Python 的 exe 包要根据目标环境的 windows 版本和位数一样才行,我只有 Mac0.0,太难了。
    ahsjs
        5
    ahsjs  
       2021-02-24 16:48:54 +08:00
    😓
    // 加上这个
    close(fileChan)

    wg := sync.WaitGroup{}
    wg.Add(totalFileNum)
    for filePathName := range fileChan {

    go func() {
    ReadExcelFile(filePathName)
    wg.Done()
    }()
    }
    ahsjs
        6
    ahsjs  
       2021-02-24 16:49:25 +08:00
    @ahsjs wg.Done 放外面
    luguhu
        7
    luguhu  
       2021-02-24 16:52:28 +08:00
    for filePathName := range fileChan {
    go utils.ReadExcelFile(filePathName, &wg)
    }
    luguhu
        8
    luguhu  
       2021-02-24 16:52:53 +08:00
    这里会 hang 住
    # 5 正解
    hsczy
        9
    hsczy  
    OP
       2021-02-24 16:57:13 +08:00
    @ahsjs 谢谢大佬。这个地方为什么要显式的关闭 chan ? chan 不是会在没有数据的时候自己关闭么?
    mybyons
        10
    mybyons  
       2021-02-24 16:57:51 +08:00
    看的好乱 还是重构一下代码吧

    ```go
    err := filepath.Walk(path, func() {
    // skip
    ...
    // do
    result := doSomethig(path)
    results = append(results, merge(result))
    })

    ....
    ```

    - Use channel when you know how to close it.
    - Don't pass wg across the boundary.
    - ....

    暂时就这些吧 不要滥用 goroutine,
    ahsjs
        11
    ahsjs  
       2021-02-24 17:36:31 +08:00
    @hsczy 同在学习中的新手😓。channel 一般在几个协程中通信使用,你这里都可以字符串数组代替。
    可以参考下官网文档 //golang.org/ref/spec#Channel_types
    如果通道就 5 条数据,range 的时候到第六条就阻塞了。还有如果你这个发送方发送超过 5 条数据也会 deadlock(不是协程)。
    katsusan
        12
    katsusan  
       2021-02-24 17:39:18 +08:00
    golang 里对 channel 的 range 要等到该 channel 被关闭才会终止。
    考虑到 channel 的收发特性一般由 sender 端关闭。

    而且我看了下 main 函数里 fileChan 容量只有 5,在工资表 xlsx 文件多于 5 个时
    下面的 fileChan <- filePathName 操作像是会一直阻塞。
    littlewey
        13
    littlewey  
       2021-02-24 19:25:20 +08:00 via iPhone
    windows 虚拟机里封一个 venv,配上 batch/powershell 调用,打包 zip,得了,不搞 exe 。
    supersu
        14
    supersu  
       2021-02-24 19:42:59 +08:00 via Android
    虚拟机装 win 呀,pyinstaller 打包 EXE 很成熟
    hsczy
        15
    hsczy  
    OP
       2021-02-24 20:06:52 +08:00
    @mybyons 学艺不精,想着用 go 和 chan 来试试没想到自己差把火,还是老老实实 for 循环单线程把事情做完了。多谢大佬指点。
    hsczy
        16
    hsczy  
    OP
       2021-02-24 20:09:06 +08:00
    @ahsjs 一起探讨,不知道我这个对不对。我的 chan 设置的缓存为 5,生产者发送第六条数据的时候不应该是在阻塞中,等到消费者消费了一条数据之后再向 chan 中发送一条数据,为什么回 deadlock 呢?
    hsczy
        17
    hsczy  
    OP
       2021-02-24 20:10:48 +08:00
    @katsusan 多谢指点。所以 for range 来操作 chan 不是应该到 chan 关闭的时候才正常退出么?如果是多个 sender 对一个 chan 进行操作的话那应该怎么来控制关闭呢?
    jworg
        18
    jworg  
       2021-02-24 21:22:14 +08:00
    如果是我的话,这个 fileChan := make(chan string, 5) 绝不会只设置 5,至少 32 我才放心。然后也不会有 for filePathName := range fileChan 这个操作,直接起 CPU 个数的循环的 consumer 协程, 线程里 for 循环不停从 fileChan 取值,加个 select 检测 1s 超时,超时就 wg.Done,fileChan 里没值就应该 5 个 consumer 协程都 done 然后退出了。
    jworg
        19
    jworg  
       2021-02-24 22:05:03 +08:00
    hysys32
        20
    hysys32  
       2021-02-25 07:47:45 +08:00 via iPhone
    简单 excel 处理 vba 不是更好的选择…
    hsczy
        21
    hsczy  
    OP
       2021-02-25 10:08:41 +08:00
    @jworg 感谢 还是要多学学多练练啊
    abccccabc
        22
    abccccabc  
       2021-02-25 10:28:09 +08:00
    @hsczy 最终的方案就是去掉 go runtinue 了?
    777777
        23
    777777  
       2021-02-25 10:54:24 +08:00
    golang 的并发很简单,但合起来就难了
    setsunakute
        24
    setsunakute  
       2021-02-25 11:32:59 +08:00
    1. 在 main 函数里的 fileChan 同时发送并接收就会导致 deadlock, channel 是不同 goroutine 之间进行通信的, 同一个 goroutine 里面就没必要使用 channel 了, append 到一个 slice 就行
    2. ReadExcelFile 函数里面的 wg.Done() 最好放在函数第一行并用 defer 来执行, 要不然中间函数因为错误返回, wg.Done()就执行不到了, 会导致 main 函数里面 wg.Wait()会一直等待, 无法结束
    3. ResultChan 要在发送端进行 close, 否则 MakeTotalExcel 这里的 range 会一直阻塞
    setsunakute
        25
    setsunakute  
       2021-02-25 11:36:57 +08:00
    你这个代码里面, fileChan 没有关闭, 于是同一个 goroutine 里面同时使用 channel 发送和接收最终会导致发送端和接收端都阻塞, 导致死锁. 可以在发送完之后, 把 fileChan close()掉, 就不会 deadlock 了, 不过还是推荐一个 slice 完事
    setsunakute
        26
    setsunakute  
       2021-02-25 11:40:48 +08:00
    @setsunakute 还是会死锁, 还是需要把 main 函数里面的 fileChan 去掉
    ZxykM
        27
    ZxykM  
       2021-02-25 12:33:31 +08:00
    我之前也遇到过,要记得把 channel 关闭掉
    no1xsyzy
        28
    no1xsyzy  
       2021-02-25 12:47:56 +08:00
    看到 chan 就先猜测滥用 chan
    果然没错
    还是 slice 吧

    go 的 chan 设计有坑,能不用就不用,反正需要的时候转 chan 比较方便,但 chan 转成其他不方便。

    #16 因为你阻塞了以后消费者还一个都没起来呢。你看一下代码,到死锁前没有一个 go 关键词

    #17 再 go 个聚合器。这个其实是 chan 的设计缺陷之一,没有设计 “无生产者” 的状态,否则的话生产者各自退出,消费者直接捕获 “无生产者” 状态就行了 —— go 调度器拥有充分的信息来断定这个 chan 已经没生产者了、消费者不可能从这个 chan 得到更多消息了。这点被好像是提出 channel 概念的论文作者亲自说了(他表示 go 确实很不错,但 chan 实在坑,缺乏他当初的论文里提到的几个基础要求)
    seran7
        29
    seran7  
       2021-02-25 17:33:53 +08:00
    楼上都说的比较全了,再补充一个代码里容易被忽视的点吧。
    utils.go 中通过 ResultChan 在 ReadExcelFile()和 MakeTotalExcel()之间传递信息,但在 ReadExcelFile()中,信息发送至通道后就执行了 wg.Done()。就算代码中没有任何错误,能够正常执行,这样的调用方式也很容易使得主程序中的 wg.Wait()等不到最后几个 MakeTotalExcel()执行完毕而提前退出。
    hsczy
        30
    hsczy  
    OP
       2021-02-25 17:40:03 +08:00
    @abccccabc 对的 0.0 直接用 map 获取数据在集中处理了
    seran7
        31
    seran7  
       2021-02-25 17:40:30 +08:00
    可以找时间看看 Concurrency in Go 这本书的第三章(尤其是 WaitGroup 和 Channels 小节),里面讲得很详细。
    hsczy
        32
    hsczy  
    OP
       2021-02-25 17:43:52 +08:00
    @setsunakute 是的 最后还是 slice 解决了问题。非常耐心感谢指导,我来消化一下
    hsczy
        33
    hsczy  
    OP
       2021-02-25 17:45:07 +08:00
    @seran7 多谢,现在就啃
    hsczy
        34
    hsczy  
    OP
       2021-02-25 17:48:08 +08:00
    @no1xsyzy 是的 第一次用就乱用一气 没有学扎实。最后还是用 slice 和 map 解决了
    hotsymbol
        35
    hotsymbol  
       2021-02-26 00:16:39 +08:00
    一个函数超过 20 行。就不想想怎么重构吗?
    hsczy
        36
    hsczy  
    OP
       2021-02-26 16:07:08 +08:00 via Android
    @hotsymbol 有道理。学无止境啊
    ji39
        37
    ji39  
       2021-03-08 17:02:17 +08:00
    头大,多线程好复杂
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1772 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 16:33 · PVG 00:33 · LAX 08:33 · JFK 11:33
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.