文章目录
更多干货,关注公众号奇伢云存储
Go 为什么没有判断 close 的接口?
相信大家初学 golang chan 的时候应该都遇到过 “send on closed channel” 的 panic 。这个 panic 是当你意图往一个已经 close 的 channel 里面投递元素的时候触发。那么你当你第一次遇到这个问题是否想过 channel 是否能提供一个接口方法来判断是否已经 close 了?我想过这个问题,但是把 chan 的源代码翻了个遍没有找到。为什么?
我先 hold 这个问题,我们捋一下跟 channel close 相关的事情,主要思考到 3 个问题:
- 关闭 channel 究竟做了什么 ?
- 怎么避免 close channel 导致的 panic 问题 ?
- 怎么优雅的关闭 channel ?
关闭 channel 究竟做了什么?
首先,用户可以 close channel,如下:
c := make(chan int)
// ...
close(c)
用 gdb 或者 delve 调试下就能发现 close 一个 channel,编译器会转换成 closechan
函数,在这个函数里是关闭 channel 的全部实现了,我们可以分析下。
closechan
对应编译函数为 closechan
,该函数很简单,大概做 3 个事情:
- 标志位置 1 ,也就是
c.closed = 1
; - 释放资源,唤醒所有等待取元素的协程;
- 释放资源,唤醒所有等待写元素的协程;
func closechan(c *hchan) {
// 以下为锁内操作
lock(&c.lock)
// 不能重复 close 一个 channel,否则 panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
// closed 标志位置 1
c.closed = 1
var glist gList
// 释放所有等待取元素的 waiter 资源
for {
// 等待读的 waiter 出队
sg := c.recvq.dequeue()
// 资源一个个销毁
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
gp := sg.g
gp.param = nil
// 相应 goroutine 加到统一队列,下面会统一唤醒
glist.push(gp)
}
// 释放所有等待写元素的 waiter 资源(他们之后将会 panic)
for {
// 等待写的 waiter 出队
sg := c.sendq.dequeue()
// 资源一个个销毁
sg.elem = nil
gp := sg.g
gp.param = nil
// 对应 goroutine 加到统一队列,下面会统一唤醒
glist.push(gp)
}
unlock(&c.lock)
// 唤醒所有的 waiter 对应的 goroutine (这个协程列表是上面 push 进来的)
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}