背景
为了批量操作数据,在流式读取数据基础上将数据写入至batch中并批量的对数据进行处理,这个过程中为了防止数据丢失,使用for select 的方式代替for range对数据进行读取,发现服务会不定期的发生空指针异常导致panic。
为什么会这样呢?
分析
根据对sarama源码的分析, 我们发现了如下内容:
ConsumeClaim must start a consumer loop of ConsumerGroupClaim's Messages().Once the Messages() channel is closed, the Handler must finish its processing loop and exit.
sarama 推荐使用for range channel处理数据,但这并不代表不能使用for select 的方式处理数据,(尽管for select 会因为通道的关闭而产生空指针异常),在这个场景我们会产生多个问题:
- 为什么从关闭的通道读取数据会读取空指针
- 为什么for range不会出现这类问题
- 如果在for select 的场景避免因通道关闭而产生panic
为什么从关闭的通道读取数据会读取空指针
不妨回忆下从通道读取/写入数据会有哪些异常情况,这里前人之述备矣直接说结论了:分别会出现阻塞、panic(仅在写入场景可能发生)、零值几种情况,其中对于无缓冲或缓冲区为空的情况,读取数据会出现阻塞,因为我们期待读到数据,可能有其他协程在一段时间后会往通道中写入数据。
而当通道关闭时,我们很确定通道中不会再有新数据(因这个时候再写入就会panic),那么当关闭的通道还有数据时,我们可以正常的读取,但当通道的数据不再存在时,我们既不能阻塞(因为不会有新数据写入,所以等待无意义),也不能直接panic(因为作为读取通道的一方,并不应该判断通道中是否还有数据),所以只能返回一个零值。
但这样我们去使用for select 时就会造成迅速的读到一个零值而不是阻塞,直到case <- time.After()生效。
为什么for range不会出现这类问题
回答这个问题,本质上要知道 for range在做什么,源码面前,没有秘密,不过这里面不是go语言源码,而是go编译器源码
// The loop we generate:
// for {
// index_temp, ok_temp = <-range
// if !ok_temp {
// break
// }
// index = index_temp
// original body
// }
这里面可以看到每次读取时都从通道中获取了两个值,通过对ok_temp的判断,及时的break
如果在for select 的场景避免因通道关闭而产生panic
通过对源码的查看我们也知道了如何解决当下的问题
for {
select {
case msg, ok := <- ch:
if !ok {
break
}
case <- time.After(time.Second):
}
}