Golang 的帮看下,运行的时候 panic,原因是写入了关闭的 channel,请问该如何解决?

2019-09-19 11:47:46 +08:00
 ngnetboy
package main

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"
)

type Detail struct {
	ID string
}

func Get(ctx context.Context, id string) (*Detail, error) {
	//you can call this function directly
	time.Sleep(time.Second * 3)
	if id == "3" {
		return nil, errors.New("error id is 3")
	}
	return &Detail{ID: id}, nil
}

func GetAll(ctx context.Context, ids []string) (map[string]*Detail, error) {
	var swg sync.WaitGroup
	result := make(map[string]*Detail, len(ids))
	detailChan := make(chan *Detail, 3)
	doneChan := make(chan struct{}, 1)
	errChan := make(chan error, 1)
	defer close(detailChan)
	defer close(doneChan)
	defer close(errChan)

	ctx, cancel := context.WithCancel(ctx)

	for _, value := range ids {
		swg.Add(1)
		go func(ctx context.Context, v string) {
			defer swg.Done()
			res, err := Get(ctx, v)
			if err != nil {
				fmt.Println("get error ", err, v)
				errChan <- err
				return
			}
			detailChan <- res
			/*
				select {
				case <-ctx.Done():
					return
				default:
					detailChan <- res
				}
			*/
		}(ctx, value)
	}

	go func() {
		for value := range detailChan {
			fmt.Println("range ", value)
			result[value.ID] = value
		}
	}()

	go func() {
		swg.Wait()
		doneChan <- struct{}{}
	}()

	select {
	case err := <-errChan:
		fmt.Println("select error:", err)
		cancel()
		return nil, err
	case <-doneChan:
		fmt.Println("select done")
	}
	return result, nil
}

func main() {
	str := []string{"1", "2", "3", "4", "5", "6"}
	GetAll(context.Background(), str)
	fmt.Println("end")
}

当执行 cancel() 的时候,会关闭 detailChan,但是 goroutine 仍然会执行,并向 detailChan 中写数据,导致 panic。

2748 次点击
所在节点    问与答
21 条回复
baiyi
2019-09-19 11:58:01 +08:00
在 goroutine 中判断下 <-ctx.Done(),收到了就 return 结束掉这个 goroutine
ngnetboy
2019-09-19 13:16:06 +08:00
@baiyi 在我代码注释的那两行里面,就是用了 <- ctx.Done() 来判断,但是好像没有什么用。
ngnetboy
2019-09-19 13:16:36 +08:00
感觉是不是我使用 context 的姿势不对?
visitant
2019-09-19 14:18:06 +08:00
detailChan 在三个槽满了的情况下,第四个 id 发送给 detailChan 被 blcok,这时发生了 err 导致 cancel()函数被执行,就会导致所有 channel 关闭吧,然后 for range 再从 detailChan 读一个数据出来,导致前一个被 detailChan 满 detailChan 的写入可以执行,就会 panic 了
ngnetboy
2019-09-19 14:25:30 +08:00
@visitant 老哥,你说的不对啊,所有的 channel 关闭之后,for range 就不会继续进行了。
xkeyideal
2019-09-19 14:31:51 +08:00
@ngnetboy 谁告诉你 channel 关闭之后,for range 此 channel 就不会执行了?
楼主学艺不精了,建议写个 case 测试一下
ngnetboy
2019-09-19 14:40:44 +08:00
@xkeyideal 哦,对,channel 关闭之后,for range 会吧缓存中的数据读取完。针对我这个问题是否有个解决方案?
ngnetboy
2019-09-19 14:45:20 +08:00
@visitant 感觉这个问题无解啊,我把缓存改成 1,并把读取的函数改成如下:还是会出现 panic
```` golang
go func(ctx context.Context) {
for value := range detailChan {
fmt.Println("range ", value)
select {
case <-ctx.Done():
return
default:
result[value.ID] = value
}
}
}(ctx)
````
pubby
2019-09-19 14:46:39 +08:00
detailChan <- res
/*
select {
case <-ctx.Done():
return
default:
detailChan <- res
}
*/


```
default:
detailChan <- res
```
改成
```
case detailChan<-res:
```
ngnetboy
2019-09-19 14:51:50 +08:00
@pubby 改成这样,仍然会 panic。
zhs227
2019-09-19 14:55:05 +08:00
go 的设计中 channel 一定要由写入方关闭, 不能由接收方关闭。写入一个关闭的 channel 会导致 panic,可以使用 recover 恢复,但不推荐这样使用。
ngnetboy
2019-09-19 14:58:37 +08:00
有一个办法就是不关闭 channel,让 GC 自动回收资源。
pubby
2019-09-19 15:09:43 +08:00
@ngnetboy 想办法先 wg.Wait,才能保证 chan 都写入了,再 close chan
ngnetboy
2019-09-19 15:17:01 +08:00
@pubby 正常情况下 chan 都写入是可以保证的,只不过出现错误的时候,就需要终止所有的操作,因为出现错误之后,剩下的操作就没有意义了。
iuoui
2019-09-19 15:31:50 +08:00
这几个地方改一下就可以了
errChan
xkeyideal
2019-09-19 15:34:54 +08:00
@ngnetboy 你这个代码写的有点乱,不想看
ngnetboy
2019-09-19 15:38:52 +08:00
@xkeyideal 老哥帮忙指导一下。
iuoui
2019-09-19 15:40:35 +08:00
这几个地方改一下就可以了,errChan 触发的时候不能马上 return,因为会触发 defer,而且 goroutine 没有退出就会 panic。
然后再 Get 方法里判断 ctx.Err==nil,并且在 detailChan 写入之前,处理一下 err==context.Canceled 情况就可以了
SAIKAII
2019-09-19 16:14:39 +08:00
手机上看代码看不清,如果是像评论里说的是 for range 的问题的话,你可以改一下。改成 for ;; v, ok = <- vchan {},然后通过判断 ok 来确定是否 chan 被关闭。记得 v 和 ok 要先声明了。虽然看起来不优雅。
ngnetboy
2019-09-19 16:14:50 +08:00
@iuoui 老哥稳,改成 cancel 之后不立即返回,等待 goroutine 完成就可以了。

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

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

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

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

© 2021 V2EX