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

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

  •  
  •   ngnetboy · 2019-09-19 11:47:46 +08:00 · 2748 次点击
    这是一个创建于 1927 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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
    baiyi
        1
    baiyi  
       2019-09-19 11:58:01 +08:00
    在 goroutine 中判断下 <-ctx.Done(),收到了就 return 结束掉这个 goroutine
    ngnetboy
        2
    ngnetboy  
    OP
       2019-09-19 13:16:06 +08:00
    @baiyi 在我代码注释的那两行里面,就是用了 <- ctx.Done() 来判断,但是好像没有什么用。
    ngnetboy
        3
    ngnetboy  
    OP
       2019-09-19 13:16:36 +08:00
    感觉是不是我使用 context 的姿势不对?
    visitant
        4
    visitant  
       2019-09-19 14:18:06 +08:00   ❤️ 1
    detailChan 在三个槽满了的情况下,第四个 id 发送给 detailChan 被 blcok,这时发生了 err 导致 cancel()函数被执行,就会导致所有 channel 关闭吧,然后 for range 再从 detailChan 读一个数据出来,导致前一个被 detailChan 满 detailChan 的写入可以执行,就会 panic 了
    ngnetboy
        5
    ngnetboy  
    OP
       2019-09-19 14:25:30 +08:00
    @visitant 老哥,你说的不对啊,所有的 channel 关闭之后,for range 就不会继续进行了。
    xkeyideal
        6
    xkeyideal  
       2019-09-19 14:31:51 +08:00
    @ngnetboy 谁告诉你 channel 关闭之后,for range 此 channel 就不会执行了?
    楼主学艺不精了,建议写个 case 测试一下
    ngnetboy
        7
    ngnetboy  
    OP
       2019-09-19 14:40:44 +08:00
    @xkeyideal 哦,对,channel 关闭之后,for range 会吧缓存中的数据读取完。针对我这个问题是否有个解决方案?
    ngnetboy
        8
    ngnetboy  
    OP
       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
        9
    pubby  
       2019-09-19 14:46:39 +08:00   ❤️ 1
    detailChan <- res
    /*
    select {
    case <-ctx.Done():
    return
    default:
    detailChan <- res
    }
    */


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