go channel 的应用可以说满是知识点,算是 golang 中的一个难点。新手使用时只要稍一不谨慎,就会造成各种问题。比如阻塞、panic、内存泄漏。接下来我将通过代码详细阐述这些问题及其解决方案。
目录
【知识点】go channel 如果没有设置缓冲队列,无论读取还是写入,都会阻塞。
如下代码所示:
func TestBlocking(t *testing.T) {
errCh := make(chan error) // 1
fmt.Println("make(chan error)")
errCh <- errors.New("chan error") // 2
fmt.Println("finish", <-errCh)
// Output:
// make(chan error)
}
上述代码会一直阻塞。因为 1 处创建了一个无缓存队列的 channel,所以代码一直阻塞在 2 处。一种解决方案是创建 channel 时使用缓冲队列(如将 1 处代码替换为 errCh := make(chan error, 1)
);一种是使用 go routine 进行发送或读取操作,以防止阻塞(如下代码所示)。
func TestWithoutBlocking(t *testing.T) {
errCh := make(chan error)
fmt.Println("make(chan error)")
go func() { errCh <- errors.New("chan error") }
fmt.Println("finish", <-errCh)
}
先看示例:
// 1.未初始化时关闭
func TestCloseNilChan(t *testing.T) {
var errCh chan error
close(errCh)
// Output:
// panic: close of nil channel
}
// 2.重复关闭
func TestRepeatClosingChan(t *testing.T) {
errCh := make(chan error)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
close(errCh)
close(errCh)
}()
wg.Wait()
// Output:
// panic: close of closed channel
}
// 3.关闭后发送
func TestSendOnClosingChan(t *testing.T) {
errCh := make(chan error)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
close(errCh)
errCh <- errors.New("chan error")
}()
wg.Wait()
// Output:
// panic: send on closed channel
}
// 4.发送时关闭
func TestCloseOnSendingToChan(t *testing.T) {
errCh := make(chan error)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
defer close(errCh)
go func() {
errCh <- errors.New("chan error") // 由于 chan 没有缓冲队列,代码会一直在此处阻塞
}()
time.Sleep(time.Second) // 等待向 errCh 发送数据
}()
wg.Wait()
// Output:
// panic: send on closed channel
}
综上,我们可以总结出如下知识点:
【知识点】在下述 4 种情况关闭 channel 会引发 panic:未初始化时关闭、重复关闭、关闭后发送、发送时关闭。
另外,从 golang 的报错中我们可以知道,golang 认为第3种和第4种情况属于一种情况。
通过观察上述代码,为避免在使用 channel 时遇到重复关闭、关闭后发送的问题,我想我们可以总结出以下两点规律:
这两点规律被称为“channel 关闭守则”。
既然关闭 channel 这么麻烦,那么我们有没有必要关闭 channel 呢?不关闭又如何?
我们考虑以下两种情况:
情况一:channel 的发送次数等于接收次数
func TestIsCloseChannelNecessary_on_equal(t *testing.T) {
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
ich := make(chan int)
// sender
go func() {
for i := 0; i < 3; i++ {
ich <- i
}
}()
// receiver
go func() {
for i := 0; i < 3; i++ {
fmt.Println(<-ich)
}
}()
time.Sleep(time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
// Output:
// NumGoroutine: 2
// 0
// 1
// 2
// NumGoroutine: 2
}
channel 的发送次数等于接收次数时,发送者 go routine 和接收者 go routine 分别都会在发送或接收结束时结束各自的 go routine。而上述代码中的 ich 会由于没有代码使用被垃圾收集器回收。因此这种情况下,不关闭 channel,没有任何副作用。
情况二:channel 的发送次数大于/小于接收次数
func TestIsCloseChannelNecessary_on_less_sender(t *testing.T) {
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
ich := make(chan int)
// sender
go func() {
for i := 0; i < 2; i++ {
ich <- i
}
}()
// receiver
go func() {
for i := 0; i < 3; i++ {
fmt.Println(<-ich)
}
}()
time.Sleep(time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
// Output:
// NumGoroutine: 2
// 0
// 1
// NumGoroutine: 3
}
以上述代码为例,channel 的发送次数小于接收次数时,接收者 go routine 由于等待发送者发送一直阻塞。因此接收者 go routine 一直未退出,ich 也由于一直被接收者使用无法被垃圾回收。未退出的 go routine 和未被回收的 channel 都造成了内存泄漏的问题。
因此,在发送者与接收者一对一的情况下,只要我们确保发送者或接收者不会阻塞,不关闭 channel 是可行的。在我们无法准确判断 channel 的发送次数和接收次数时,我们应该在合适的时机关闭 channel。那么如何判断 channel 是否关闭呢?
【知识点】go channel 关闭后,读取该 channel 永远不会阻塞,且只会输出对应类型的零值。
如下代码所示:
func TestReadFromClosedChan(t *testing.T) {
var errCh = make(chan error)
go func() {
defer close(errCh)
errCh <- errors.New("chan error")
}()
go func() {
for i := 0; i < 3; i++ {
fmt.Println(i, <-errCh)
}
}()
time.Sleep(time.Second)
// Output:
// 0 chan error
// 1
// 2
}
以上述代码为例,nil 可能也是需要 channel传输的值之一,通常我们无法通过判断是否为类型的零值确定 channel 是否关闭。所以为了避免输出无意义的值,我们需要一种合理的方式判断 channel 是否关闭。golang 官方为我们提供了两种方式。
解决方案一:使用 channel 的多重返回值(如 err, ok := <-errCh )
func TestReadFromClosedChan2(t *testing.T) {
var errCh = make(chan error)
go func() {
defer close(errCh)
errCh <- errors.New("chan error")
}()
go func() {
for i := 0; i < 3; i++ {
err, ok := <-errCh
if ok {
fmt.Println(i, err)
} else {
fmt.Println(i, err)
}
}
}()
time.Sleep(time.Second)
// Output:
// 0 chan error
// 1
// 2
}
err, ok := <-errCh 的第二个返回值 ok 表示 errCh 是否已经关闭。如果已关闭,则返回 false。
解决方案二:使用 for range 简化语法
func TestReadFromClosedChan(t *testing.T) {
var errCh = make(chan error)
go func() {
defer close(errCh)
errCh <- errors.New("chan error")
}()
go func() {
i := 0
for err := range errCh {
fmt.Println(i, err)
i++
}
}()
time.Sleep(time.Second)
// Output:
// 0 chan error
}
for range 语法会自动判断 channel 是否结束,如果结束则自动退出 for 循环。
我们从前文也了解到,如果发生重复关闭、关闭后发送等问题,会造成 channel panic。那么如何优雅地关闭 channel,是我们关心的一个问题。
golang 官方为我们提供了一种方式,可以用来尽量避免这个问题。golang 允许我们使用 <-
控制 channel 发送方向,防止我们在错误的时候关闭 channel。
func TestOneSenderOneReceiver(t *testing.T) {
ich := make(chan int)
go sender(ich)
go receiver(ich)
}
func sender(ich chan<- int) {
for i := 0; i < 100; i++ {
ich <- i
}
}
func receiver(ich <-chan int) {
fmt.Println(<-ich)
close(ich) // 此处代码会在编译期报错
}
使用这种方法时,由于 close()
函数只能接受 chan<- T
类型的 channel,如果我们尝试在接收方关闭 channel,编译器会报错,所以我们可以在编译期提前发现错误。
除此之外,我们也可以使用如下的结构体(抄自go101《如何优雅地关闭 go channels》,做了一点修改,链接为此文的中文翻译):
type Channel struct {
C chan interface{}
closed bool
mut sync.Mutex
}
func NewChannel() *Channel {
return NewChannelSize(0)
}
func NewChannelSize(size int) *Channel {
return &Channel{
C: make(chan interface{}, size),
closed: false,
mut: sync.Mutex{},
}
}
func (c *Channel) Close() {
c.mut.Lock()
defer c.mut.Unlock()
if !c.closed {
close(c.C)
c.closed = true
}
}
func (c *Channel) IsClosed() bool {
c.mut.Lock()
defer c.mut.Unlock()
return c.closed
}
func TestChannel(t *testing.T) {
ch := NewChannel()
println(ch.IsClosed())
ch.Close()
ch.Close()
println(ch.IsClosed())
}
该方案可以解决重复关闭锁的问题以及锁是否关闭的问题。通过 Channel.IsClosed()
判断是否关闭 channel ,又可以安全地发送和接收。当然我们也可以把 sync.Mutex
换成 sync.Once
,来只让 channel 关闭一次。具体可以参考《如何优雅地关闭 go channels》。
有时候我们的代码已经使用了原生的 chan
,或者我们不想使用单独的数据结构,也可以使用下述的几种方案。通常情况下,我们只会遇到四种需要关闭 channel 的情况(以下内容是我对《如何优雅地关闭 go channels》中方法的总结):
因此我们只需要熟记面对这四种情况时如何关闭 channel 即可。为避免单纯地抄袭,具体的代码实现可以去参考《如何优雅地关闭 go channels》这篇文章(划到中间位置,找“保持channel closing principle的优雅方案”关键字即可)。
代码不会撒谎。事实证明,使用 go channel 要注意的问题确实不少。新手使用时只要稍一不谨慎,就会造成各种问题。即便是老手,在使用 go channel 时也少不了会造成内存泄漏的问题。后续我会再写一篇文章来详细讨论 go channel 可能造成的内存泄漏的问题。但这都不重要,重要的是:各位老爷,给个赞吧!