go 10行代码踩遍channel的3个panic

Posted historyofsmile

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了go 10行代码踩遍channel的3个panic相关的知识,希望对你有一定的参考价值。

go 10行代码踩遍channel的3个panic

0、nil channel的阻塞

开发的时候,由于疏忽,没有初始化channel,写出如下代码:

func main() {
 var ch chan int // nil channel
 go func() {
  ch <- 123 // 阻塞
 }()

 fmt.Println(<-ch) // 阻塞
}

开始执行,发现程序完全阻塞,除此之外,没有任何其他异常——读写 nil channel都会阻塞,不会panic

尝试一下关闭这个channel会发生什么。

1、关闭一个 nil channel —— panic

在上面的代码中添加一个关闭channel的函数:

func main() {
 var ch chan int // nil channel
 close(ch)       // panic: close of nil channel    /*1*/
 go func() {
  ch <- 123
 }()

 fmt.Println(<-ch)
}

这一个close操作,直接操作出个panic,好吓人,仔细一看,哦,原来我们的channel只是定义了,并没有初始化,它只是个nil channel,那么可知:close 一个 nil channel 会报panic: close of nil channel

如果我们把这个channel 初始化了再关闭呢?它应该就不会报这个panic了吧,说干就干。

2、向一个closed channel 发送数据 —— panic

在上一步的基础上,给channel 做了个初始化操作,如下:

func main() {
 var ch chan int
 ch = make(chan int)     /*2*/
 close(ch) // closed     /*1*/

 go func() {
  ch <- 123 // panic: send on closed channel
 }()

 fmt.Println(<-ch) // 这里仍然可以读取到数据,为int的默认值 —— closed 的channel 可以读取到数据(默认值)
}

surprise!主函数执行完了,那么意味着channel中读到了值,但是读到的并不是我们写入的123,而是0

但是,同时,程序抛出一个**panic: send on closed channel。可见,往一个 已经关闭的channel中写数据,直接会panic** 。

接下来,由于手滑,close(ch)被我多复制了一行,你猜怎么着?

3、关闭一个已经关闭的channel —— panic

func main() {
 var ch chan int
 ch = make(chan int)		/*2*/
 close(ch)					 /*1*/
 close(ch) // panic: close of closed channel	 /*3*/

 go func() {
  ch <- 123
 }()

 fmt.Println(<-ch)
}

这下倒好,程序根本就执行不到末尾,在第二个 close(ch)处就panic了——panic: close of closed channel,可见,关闭已经被关闭的channel会panic

这一步操作纯属失误,我申请悔棋,代码回退到步骤2,那里至少有机会把函数执行完不是吗?

4、在一个合适的时机关闭channel(正确示范)

经过上面的一系列操作,你会发现,这个程序中,你即便是不关闭这个channel,程序也能无伤地执行完。例如:

func main() {
 var ch chan int
 ch = make(chan int)
 go func() {
  ch <- 123
 }()

 fmt.Println(<-ch) // 123
}

但是,作为一个负责人的gopher,总觉得这么处理有些不妥当,平时写goroutine的原则是:谁负责创建,谁就要负责释放。使用channel也应该养成类似的习惯,最好能自己动手关掉它。

例如,在channel使用完成的时候关闭它:

func main() {
 var ch chan int
 ch = make(chan int)
 defer close(ch)  /*4*/
 go func() {
  ch <- 123
 }()

 fmt.Println(<-ch) // 123
}

或者在channel数据发送完的时候。我们知道关闭掉channel,并不影响接收数据,那么发送完成后就关闭掉它,安全又妥当。例如:

func main() {
 var ch chan int
 ch = make(chan int)
 go func() {
 defer close(ch)  /*4'*/
  ch <- 123
 }()

 fmt.Println(<-ch) // 123
}

总结

channel 的panic

channel的panic有3中情形,两个和close()操作有关,两个和closed状态有关。so,操作channel的时候一定要注意关于关闭(的)操作。

  1. **panic: close of nil channel **
  2. panic: send on closed channel
  3. panic: close of closed channel

nil channel

刚开就交代了nil channel 的情况,无论发送还是接收,都会阻塞,切记使用channel要make初始化,如果遗漏了这个操作,排查阻塞可不像panic那么容易。

接收closed channel

  1. 已经关闭的channel仍然可以接收,这也是channel的另一个妙用,可以用它作为1:N的通知器,例如:
func main(){
	ch:=make(chan struct{})

	for i:=0;i<5;i++{
		go func(i int) {
			<-ch // goroutine 阻塞在此,等待信号,一起向下执行
			fmt.Println(i," do something ...")
		}(i )
	}

	close(ch) // 关闭 channel,通知goroutine都向下执行
	time.Sleep(time.Second)
}
  1. close()操作也是 range操作的终止信号:
func main() {
	ch := make(chan int)

	go func() {
		defer close(ch) // 数据发送完之后,就关闭channel
		for i := 0; i < 5; i++ {
			ch <- i
		}
	}()

	// channel 也有类似断言一样的操作,第二个值为当前channel的状态,如果已经关闭,则为false
	v, ok := <-ch
	if ok {
		fmt.Println("v, ok := <-ch ---> ",v)
	} else {
		return
	}

	// 数据接收到 close 时刻的,就不在循环获取,退出range
	for value := range ch {
		fmt.Println("range ---> ",value)
	}
}

结果如下:
v, ok := <-ch --->  0
range --->  1
range --->  2
range --->  3
range --->  4

以上是关于go 10行代码踩遍channel的3个panic的主要内容,如果未能解决你的问题,请参考以下文章

go 10行代码踩遍channel的3个panic

go 10行代码踩遍channel的3个panic

Go36-21,22-panic函数recover函数以及defer语句

golang channel tips

3. Go中panic与recover注意事项

2022-10-16:以下go语言代码输出什么?A:timed out;B:panic;C:没有任何输出。 package main import ( “context“ “fmt“