首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
V2EX  ›  问与答

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

  •  
  •   ngnetboy · 81 天前 · 1110 次点击
    这是一个创建于 81 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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。

    21 回复  |  直到 2019-09-19 19:35:47 +08:00
        1
    baiyi   81 天前
    在 goroutine 中判断下 <-ctx.Done(),收到了就 return 结束掉这个 goroutine
        2
    ngnetboy   81 天前
    @baiyi 在我代码注释的那两行里面,就是用了 <- ctx.Done() 来判断,但是好像没有什么用。
        3
    ngnetboy   81 天前
    感觉是不是我使用 context 的姿势不对?
        4
    visitant   81 天前   ♥ 1
    detailChan 在三个槽满了的情况下,第四个 id 发送给 detailChan 被 blcok,这时发生了 err 导致 cancel()函数被执行,就会导致所有 channel 关闭吧,然后 for range 再从 detailChan 读一个数据出来,导致前一个被 detailChan 满 detailChan 的写入可以执行,就会 panic 了
        5
    ngnetboy   81 天前
    @visitant 老哥,你说的不对啊,所有的 channel 关闭之后,for range 就不会继续进行了。
        6
    xkeyideal   81 天前
    @ngnetboy 谁告诉你 channel 关闭之后,for range 此 channel 就不会执行了?
    楼主学艺不精了,建议写个 case 测试一下
        7
    ngnetboy   81 天前
    @xkeyideal 哦,对,channel 关闭之后,for range 会吧缓存中的数据读取完。针对我这个问题是否有个解决方案?
        8
    ngnetboy   81 天前
    @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)
    ````
        9
    pubby   81 天前   ♥ 1
    detailChan <- res
    /*
    select {
    case <-ctx.Done():
    return
    default:
    detailChan <- res
    }
    */


    ```
    default:
    detailChan <- res
    ```
    改成
    ```
    case detailChan<-res:
    ```
        10
    ngnetboy   81 天前
    @pubby 改成这样,仍然会 panic。
        11
    zhs227   81 天前
    go 的设计中 channel 一定要由写入方关闭, 不能由接收方关闭。写入一个关闭的 channel 会导致 panic,可以使用 recover 恢复,但不推荐这样使用。
        12
    ngnetboy   81 天前
    有一个办法就是不关闭 channel,让 GC 自动回收资源。
        13
    pubby   81 天前 via Android
    @ngnetboy 想办法先 wg.Wait,才能保证 chan 都写入了,再 close chan
        14
    ngnetboy   81 天前
    @pubby 正常情况下 chan 都写入是可以保证的,只不过出现错误的时候,就需要终止所有的操作,因为出现错误之后,剩下的操作就没有意义了。
        15
    iuoui   81 天前
    这几个地方改一下就可以了
    errChan
        16
    xkeyideal   81 天前
    @ngnetboy 你这个代码写的有点乱,不想看
        17
    ngnetboy   81 天前
    @xkeyideal 老哥帮忙指导一下。
        18
    iuoui   81 天前   ♥ 1
    这几个地方改一下就可以了,errChan 触发的时候不能马上 return,因为会触发 defer,而且 goroutine 没有退出就会 panic。
    然后再 Get 方法里判断 ctx.Err==nil,并且在 detailChan 写入之前,处理一下 err==context.Canceled 情况就可以了
        19
    SAIKAII   81 天前 via Android
    手机上看代码看不清,如果是像评论里说的是 for range 的问题的话,你可以改一下。改成 for ;; v, ok = <- vchan {},然后通过判断 ok 来确定是否 chan 被关闭。记得 v 和 ok 要先声明了。虽然看起来不优雅。
        20
    ngnetboy   81 天前
    @iuoui 老哥稳,改成 cancel 之后不立即返回,等待 goroutine 完成就可以了。
        21
    visitant   81 天前 via iPhone
    @ngnetboy 这跟你缓存是开了多大根本没关系,关键在于 select 里 case <-ctx.Done() 然后你还有个 default,导致协程根本没被 select blcok,而是被 res 写 chan 的操作 block
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1152 人在线   最高记录 5043   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.3 · 26ms · UTC 23:21 · PVG 07:21 · LAX 15:21 · JFK 18:21
    ♥ Do have faith in what you're doing.