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。
1
baiyi 2019-09-19 11:58:01 +08:00
在 goroutine 中判断下 <-ctx.Done(),收到了就 return 结束掉这个 goroutine
|
3
ngnetboy OP 感觉是不是我使用 context 的姿势不对?
|
4
visitant 2019-09-19 14:18:06 +08:00 1
detailChan 在三个槽满了的情况下,第四个 id 发送给 detailChan 被 blcok,这时发生了 err 导致 cancel()函数被执行,就会导致所有 channel 关闭吧,然后 for range 再从 detailChan 读一个数据出来,导致前一个被 detailChan 满 detailChan 的写入可以执行,就会 panic 了
|
6
xkeyideal 2019-09-19 14:31:51 +08:00
@ngnetboy 谁告诉你 channel 关闭之后,for range 此 channel 就不会执行了?
楼主学艺不精了,建议写个 case 测试一下 |
7
ngnetboy OP @xkeyideal 哦,对,channel 关闭之后,for range 会吧缓存中的数据读取完。针对我这个问题是否有个解决方案?
|
8
ngnetboy OP |
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: ``` |
11
zhs227 2019-09-19 14:55:05 +08:00
go 的设计中 channel 一定要由写入方关闭, 不能由接收方关闭。写入一个关闭的 channel 会导致 panic,可以使用 recover 恢复,但不推荐这样使用。
|
12
ngnetboy OP 有一个办法就是不关闭 channel,让 GC 自动回收资源。
|
14
ngnetboy OP @pubby 正常情况下 chan 都写入是可以保证的,只不过出现错误的时候,就需要终止所有的操作,因为出现错误之后,剩下的操作就没有意义了。
|
15
iuoui 2019-09-19 15:31:50 +08:00
这几个地方改一下就可以了
errChan |
18
iuoui 2019-09-19 15:40:35 +08:00 1
这几个地方改一下就可以了,errChan 触发的时候不能马上 return,因为会触发 defer,而且 goroutine 没有退出就会 panic。
然后再 Get 方法里判断 ctx.Err==nil,并且在 detailChan 写入之前,处理一下 err==context.Canceled 情况就可以了 |
19
SAIKAII 2019-09-19 16:14:39 +08:00 via Android
手机上看代码看不清,如果是像评论里说的是 for range 的问题的话,你可以改一下。改成 for ;; v, ok = <- vchan {},然后通过判断 ok 来确定是否 chan 被关闭。记得 v 和 ok 要先声明了。虽然看起来不优雅。
|