Golang 新手求助

2021-02-24 16:29:10 +08:00
 hsczy

帮朋友做 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")
}

3955 次点击
所在节点    Go 编程语言
37 条回复
hsczy
2021-02-25 10:08:41 +08:00
@jworg 感谢 还是要多学学多练练啊
abccccabc
2021-02-25 10:28:09 +08:00
@hsczy 最终的方案就是去掉 go runtinue 了?
777777
2021-02-25 10:54:24 +08:00
golang 的并发很简单,但合起来就难了
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
2021-02-25 11:36:57 +08:00
你这个代码里面, fileChan 没有关闭, 于是同一个 goroutine 里面同时使用 channel 发送和接收最终会导致发送端和接收端都阻塞, 导致死锁. 可以在发送完之后, 把 fileChan close()掉, 就不会 deadlock 了, 不过还是推荐一个 slice 完事
setsunakute
2021-02-25 11:40:48 +08:00
@setsunakute 还是会死锁, 还是需要把 main 函数里面的 fileChan 去掉
ZxykM
2021-02-25 12:33:31 +08:00
我之前也遇到过,要记得把 channel 关闭掉
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
2021-02-25 17:33:53 +08:00
楼上都说的比较全了,再补充一个代码里容易被忽视的点吧。
utils.go 中通过 ResultChan 在 ReadExcelFile()和 MakeTotalExcel()之间传递信息,但在 ReadExcelFile()中,信息发送至通道后就执行了 wg.Done()。就算代码中没有任何错误,能够正常执行,这样的调用方式也很容易使得主程序中的 wg.Wait()等不到最后几个 MakeTotalExcel()执行完毕而提前退出。
hsczy
2021-02-25 17:40:03 +08:00
@abccccabc 对的 0.0 直接用 map 获取数据在集中处理了
seran7
2021-02-25 17:40:30 +08:00
可以找时间看看 Concurrency in Go 这本书的第三章(尤其是 WaitGroup 和 Channels 小节),里面讲得很详细。
hsczy
2021-02-25 17:43:52 +08:00
@setsunakute 是的 最后还是 slice 解决了问题。非常耐心感谢指导,我来消化一下
hsczy
2021-02-25 17:45:07 +08:00
@seran7 多谢,现在就啃
hsczy
2021-02-25 17:48:08 +08:00
@no1xsyzy 是的 第一次用就乱用一气 没有学扎实。最后还是用 slice 和 map 解决了
hotsymbol
2021-02-26 00:16:39 +08:00
一个函数超过 20 行。就不想想怎么重构吗?
hsczy
2021-02-26 16:07:08 +08:00
@hotsymbol 有道理。学无止境啊
ji39
2021-03-08 17:02:17 +08:00
头大,多线程好复杂

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/755876

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX