func closechan(c *hchan) {
// 关闭一个 nil channel, 抛出 panic
if c == nil {
panic(plainError("close of nil channel"))
}
}
为什么关闭一个已经关闭的 channel
会 panic
?
官方这样设计的初衷,应该是希望开发者不要依赖于 close
函数,而是要求开发者通过合理设计 goroutine + channel
工作流来提高程序的健壮性。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
if val, ok := <-ch; !ok {
// channel 已关闭
fmt.Println("channel closed")
} else {
fmt.Printf("val = %d", val)
}
}()
ch <- 1024
close(ch)
time.Sleep(time.Second)
}
关闭一个已经关闭的 channel 会 panic, 实现一个方法,可以让调用方无需考虑边界情况,直接调用即可。
下面的代码只是作为技术解决方案探究,没有任何实际意义 (不要应用在你的任何业务代码中)。
通过 recover 函数捕获 panic, 可以保证关闭一个已经关闭的 channel 报错不会导致程序终止。
package main
import (
"fmt"
)
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("recover err: %v\n", err)
}
}()
ch := make(chan int)
close(ch)
// 关闭一个已经关闭的 channel
close(ch)
}
通过 sync.Once 方法保证 close(channel) 只会被调用一次。
package main
import (
"sync"
)
type myChan struct {
ch chan int
once sync.Once
}
func (c *myChan) close() {
c.once.Do(func() {
close(c.ch)
})
}
func main() {
ch := &myChan{
ch: make(chan int),
}
ch.close()
// 关闭一个已经关闭的 channel
ch.close()
}
通过 atomic.CAS 方法保证 close(channel) 只会被调用一次。
package main
import "sync/atomic"
type myChan struct {
ch chan int
closed int32
}
func (c *myChan) close() {
if atomic.CompareAndSwapInt32(&c.closed, 0, 1) {
close(c.ch)
}
}
func main() {
ch := &myChan{
ch: make(chan int),
}
ch.close()
// 关闭一个已经关闭的 channel
ch.close()
}
通过 context.Context 保证 close(channel) 的操作顺序同步。
package main
import (
"context"
)
type myChan struct {
ch chan int
ctx context.Context
cancel context.CancelFunc
}
func (c *myChan) close() {
select {
case <-c.ctx.Done():
return
default:
close(c.ch)
// 事件同步
c.cancel()
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
ch := &myChan{
ch: make(chan int),
ctx: ctx,
cancel: cancel,
}
ch.close()
// 关闭一个已经关闭的 channel
ch.close()
}
select + default
处理多个 channel
轮询场景channel
, 只在写入端关闭 channelchannel
关闭检测方法函数,应该将 channel
的读写操作进行分离 (通过不同的 goroutine
),并实现只在一个写入端关闭 channelcontext.Context
控制 channel
的生命周期channel
的使用场景channel
提供了阻塞机制,虽然避免了数据竞态,但是当数据较多时降低了性能,而且可能引发死锁channel
虽然避免了阻塞,但是有潜在的数据竞态,而且需要考虑缓冲区大小,设计不合理容易浪费资源当你发现使用锁使程序变得复杂时,可以试试使用 channel 会不会使程序变得简单。
官方给出的建议是除了特殊的、底层的应用程序外,其他情况最好使用 channel
或其他同步原语来完成 (但是从大多数开源组件实现代码来看,并没有遵守官方的建议)。