一、前言
前几天写了篇文章,是通过sync.Map
获取goroutine
的返回结果然后做出处理,但是一直感觉方案一般,不是很好。毕竟channel
才是钦定的太子,所以还是用channel
好一些。
二、误区以及实战代码
1、误区
博主自己用channel
一般都是用来控制goroutine
的并发,所以channel
结构比较简单,就想当然的认为channel
只适合存储简单的结构,复杂的函数处理结果通过channel
处理不太方便,是在是谬之千里。
2.实战代码
注:以下为脱敏的伪代码
代码含义概览:
(1)通过channel控制goroutine数量
(2)定义channel结构体,结构体里面可以根据需求嵌套其他结构体,实现我们想要的复杂结构
(3)每次循环获取go函数处理结果,处理每次循环结果
//EsBulkDataRes 可以定义复杂的结构,或者嵌套结构体
type EsBulkDataRes struct {
SucceededNums int `json:"succeeded_nums"`
FailedNums int `json:"failed_nums"`
ErrData string `json:"err_data"`
//TestDoc *TestDoc
}
type TestDoc struct {
ID string `json:"_id,omitempty"`
Source map[string]interface{} `json:"_source,omitempty"` //map里面可以存储复杂的结构
}
func testChannel() {
//定义要获取的返回值,成功数量,失败数量,失败id集合
succeededNums, failedNums := 0, 0
var errData string
DealRes := make(chan EsBulkDataRes)
defer func() {
close(DealRes)
}()
wg := sync.WaitGroup{}
//控制goroutine数量,保证同时只有10个goroutine
chan1 := make(chan struct{}, 10)
ctx := context.Background()
for {
//业务逻辑
// ....
//goroutine加速,chan1写满,则阻塞。等待之前的goroutine释放才能继续循环
chan1 <- struct{}{}
wg.Add(1)
go func(ctx context.Context, targetIndexName string, esClient *elastic.Client) {
defer func() {
if err1 := recover(); err1 != nil { //产生了panic异常
log.Errorf(ctx, "%s go panic! err:(+%v)", logPreFix, err1)
}
//执行完毕再释放channel
<-chan1
return
}()
bulkRequest := esClient.Bulk()
//ES使用bulk方法批量刷新数据
bulkResByAssetInfo := new(EsBulkDataRes)
bulkResByAssetInfo, err = BulkEsDataByAssetInfo(ctx, bulkRequest, targetIndexName)
if err != nil {
log.Errorf(ctx, "%s BulkEsDataByAssetInfo error (%+v) ,test:(+%v)", logPreFix, err, string_util.TransferToString(fileUnitList))
return
}
//执行结果写入channel
DealRes <- *bulkResByAssetInfo
//执行完毕再释放channel
<-chan1
}(ctx, targetIndexName, esClient)
//goroutine 执行结束,读取channel累加结果
//读取channel,也是为了方便下一个 goroutine 的写入。没读的话,会阻塞
select {
case d, ok := <-DealRes:
if !ok {
continue
}
//累加结果
succeededNums += d.SucceededNums
failedNums += d.FailedNums
errData += d.ErrData
case <-ctx.Done():
return
}
}
wg.Wait()
//打印结果
fmt.Println("成功数量:", succeededNums)
fmt.Println("失败数量:", failedNums)
fmt.Println("失败id:", errData)
}
三、channel控制多个goroutine串行
这部分是博主之前碰到的面试题,说多个go
函数,每个go
函数都依赖于上一步的处理结果,如何实现串行。此处给出伪代码,参考下即可。
//定义channel结构
type chanStruct struct {
Res1 int64
}
//每个函数中重新给channel赋值
func test1(chan1 chan chanStruct) {
res1 := new(chanStruct)
fmt.Println("test1")
res1.Res1 = 2
chan1 <- *res1
return
}
func test2(chan2 chan chanStruct) {
res2 := new(chanStruct)
fmt.Println("test2")
res2.Res1 = 3
chan2 <- *res2
return
}
func test3(chan3 chan chanStruct) {
fmt.Printf("test3,chanStruct:(%+v)", chan3)
return
}
//https://segmentfault.com/q/1010000041024462/
//无缓冲通道读写都必须在协程里,否则会阻塞。有缓冲通道则可以不需要都准备好,读或者写可以写在当前线程里而不会阻塞。
func main() {
chan0 := make(chan chanStruct, 1)
//这里使用了"golang.org/x/sync/errgroup" 这个包,博主个人实验,在此处非必需
g, ctx := errgroup.WithContext(context.Background())
chan0 <- chanStruct{
Res1: 1,
}
fmt.Println("write chan success!")
//errgroup控制并发,并获取goroutine返回的错误(此处没用到)
g.Go(func() error {
for {
select {
//注意这里,由于每次我们都读出来了channel,因此需要在函数中给channel赋值
//保证能触发下一个函数
case d, ok := <-chan0:
fmt.Println("d:", d)
if ok {
if d.Res1 == 1 {
go test1(chan0)
} else if d.Res1 == 2 {
go test2(chan0)
} else if d.Res1 == 3 {
go test3(chan0)
fmt.Println("end")
return nil
}
}
case <-ctx.Done():
return ctx.Err()
}
}
})
//errgroup的Wait类似于sync.withGroup的Wait()方法,等待goroutine执行结束
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}
四、后记
我们该如何确认自己写的代码比较好呢?这个好又要如何定义?只是实现功能还是说要保持优雅? 以上是博主跟一个大佬聊天的时候大佬问的问题。
对于我们开发者来说,以实现需求为第一目的是绝对没问题的,但是代码质量也需要持续提升。怎么提升呢,当然是看大佬的代码!哪里大佬的代码最多呢,当然是github!
博主最近看了https://github.com/olivere/esdiff
大佬的代码,才发现自己以前的狭隘,也惊叹于大佬的写法之妙。这还只是个不知名的开源项目,不知道k8s,etcd等知名项目又会是怎样的波澜壮阔!加油!
end