帮朋友做 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")
}
1
luguhu 2021-02-24 16:41:30 +08:00
因为 channel 没有 Close 吧
|
2
hsczy OP @luguhu 使用 channel 需要 close 么?我看教程上面没有显式关闭的模式。如果要关闭的话 是在 wg.wait()之后关闭么?
|
3
darksword21 2021-02-24 16:45:50 +08:00
我觉得还是 python 好写一些。。
|
4
hsczy OP @darksword21 Python 的 exe 包要根据目标环境的 windows 版本和位数一样才行,我只有 Mac0.0,太难了。
|
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() }() } |
7
luguhu 2021-02-24 16:52:28 +08:00
for filePathName := range fileChan {
go utils.ReadExcelFile(filePathName, &wg) } |
8
luguhu 2021-02-24 16:52:53 +08:00
这里会 hang 住
# 5 正解 |
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, |
11
ahsjs 2021-02-24 17:36:31 +08:00
@hsczy 同在学习中的新手😓。channel 一般在几个协程中通信使用,你这里都可以字符串数组代替。
可以参考下官网文档 //golang.org/ref/spec#Channel_types 如果通道就 5 条数据,range 的时候到第六条就阻塞了。还有如果你这个发送方发送超过 5 条数据也会 deadlock(不是协程)。 |
12
katsusan 2021-02-24 17:39:18 +08:00
golang 里对 channel 的 range 要等到该 channel 被关闭才会终止。
考虑到 channel 的收发特性一般由 sender 端关闭。 而且我看了下 main 函数里 fileChan 容量只有 5,在工资表 xlsx 文件多于 5 个时 下面的 fileChan <- filePathName 操作像是会一直阻塞。 |
13
littlewey 2021-02-24 19:25:20 +08:00 via iPhone
windows 虚拟机里封一个 venv,配上 batch/powershell 调用,打包 zip,得了,不搞 exe 。
|
14
supersu 2021-02-24 19:42:59 +08:00 via Android
虚拟机装 win 呀,pyinstaller 打包 EXE 很成熟
|
15
hsczy OP @mybyons 学艺不精,想着用 go 和 chan 来试试没想到自己差把火,还是老老实实 for 循环单线程把事情做完了。多谢大佬指点。
|
16
hsczy OP @ahsjs 一起探讨,不知道我这个对不对。我的 chan 设置的缓存为 5,生产者发送第六条数据的时候不应该是在阻塞中,等到消费者消费了一条数据之后再向 chan 中发送一条数据,为什么回 deadlock 呢?
|
17
hsczy OP @katsusan 多谢指点。所以 for range 来操作 chan 不是应该到 chan 关闭的时候才正常退出么?如果是多个 sender 对一个 chan 进行操作的话那应该怎么来控制关闭呢?
|
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 然后退出了。
|
19
jworg 2021-02-24 22:05:03 +08:00
|
20
hysys32 2021-02-25 07:47:45 +08:00 via iPhone
简单 excel 处理 vba 不是更好的选择…
|
23
777777 2021-02-25 10:54:24 +08:00
golang 的并发很简单,但合起来就难了
|
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 会一直阻塞 |
25
setsunakute 2021-02-25 11:36:57 +08:00
你这个代码里面, fileChan 没有关闭, 于是同一个 goroutine 里面同时使用 channel 发送和接收最终会导致发送端和接收端都阻塞, 导致死锁. 可以在发送完之后, 把 fileChan close()掉, 就不会 deadlock 了, 不过还是推荐一个 slice 完事
|
26
setsunakute 2021-02-25 11:40:48 +08:00
@setsunakute 还是会死锁, 还是需要把 main 函数里面的 fileChan 去掉
|
27
ZxykM 2021-02-25 12:33:31 +08:00
我之前也遇到过,要记得把 channel 关闭掉
|
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 实在坑,缺乏他当初的论文里提到的几个基础要求) |
29
seran7 2021-02-25 17:33:53 +08:00
楼上都说的比较全了,再补充一个代码里容易被忽视的点吧。
utils.go 中通过 ResultChan 在 ReadExcelFile()和 MakeTotalExcel()之间传递信息,但在 ReadExcelFile()中,信息发送至通道后就执行了 wg.Done()。就算代码中没有任何错误,能够正常执行,这样的调用方式也很容易使得主程序中的 wg.Wait()等不到最后几个 MakeTotalExcel()执行完毕而提前退出。 |
31
seran7 2021-02-25 17:40:30 +08:00
可以找时间看看 Concurrency in Go 这本书的第三章(尤其是 WaitGroup 和 Channels 小节),里面讲得很详细。
|
32
hsczy OP @setsunakute 是的 最后还是 slice 解决了问题。非常耐心感谢指导,我来消化一下
|
35
hotsymbol 2021-02-26 00:16:39 +08:00
一个函数超过 20 行。就不想想怎么重构吗?
|
37
ji39 2021-03-08 17:02:17 +08:00
头大,多线程好复杂
|