Go语言or-done-channel模式疑问:内部select无return及双通道就绪情况
先贴出对应的实现代码方便参考:
orDone := func(done, c <-chan interface{}) <-chan interface{} { valStream := make(chan interface{}) go func() { defer close(valStream) for { select { //2 case <-done: return case v, ok := <-c: if ok == false { return } select { case valStream <- v: //3 case <-done: //1 } } } }() return valStream }
问题1:为何内部select的<-done分支没有像外层select那样添加return语句?若没有return,下一轮迭代中若done和c通道均就绪,select会随机选择分支,是否可能在done就绪时仍选择<-c?
内部select的<-done分支不需要加return,原因是:当这个分支被触发时,当前已经从c中读取了值v但没写入valStream,此时goroutine会回到外层的for循环,进入外层的select(标记//2)。而此时done通道已经关闭,<-done会持续处于就绪状态——外层select的两个分支如果同时就绪(c还有未读数据、done已关闭),确实存在某次随机选中<-c分支的可能,但这种情况不会持续发生:只要done保持就绪状态,外层select总有一次会选中<-done分支,执行return终止goroutine,不会无限读取c。
简单说,内部分支不return是因为外层循环的done分支会最终兜底终止goroutine,没必要在内部提前return。
问题2:当done和c通道同时就绪时,select会随机选择分支,是否可以认为done分支最终会被选中,但不一定在done关闭后立即被选中?
完全正确。Go的select语句在多个分支同时就绪时,会随机选择一个执行。当done关闭后,<-done会一直处于就绪状态,所以后续每次进入外层select,都有概率选中done分支。哪怕前几次偶然选中了c分支,只要done保持就绪,最终必然会有一次选中done分支,goroutine就会退出,只是这个过程不一定在done关闭后立刻发生。
问题3:当done关闭时,是否存在已读取的v未写入valStream而永久丢失的情况?
是的,确实存在这种数据丢失的场景。比如:
- 外层
select选中<-c分支,成功读取到v; - 进入内部
select时,done已经关闭,此时内部的两个分支(valStream <-v和<-done)同时就绪; select随机选中了<-done分支,此时v没有被写入valStream;- 之后goroutine回到外层循环,外层
select选中done分支执行return,valStream被关闭,这个v就永久丢失了。
这属于这个实现的一个小“漏洞”——已经从源通道c消费了数据,但没有传递到目标通道valStream,导致数据丢失。如果要避免这个问题,可以在内部done分支中把v处理掉(比如写入缓冲或明确丢弃,需结合业务逻辑),不过原模式的设计可能更关注goroutine的及时终止,而非数据的绝对不丢失。
内容的提问来源于stack exchange,提问作者user10089194

