GO系列
1、GO学习之Hello World
2、GO学习之入门语法
3、GO学习之切片操作
4、GO学习之 Map 操作
5、GO学习之 结构体 操作
6、GO学习之 通道(Channel)
7、GO学习之 多线程(goroutine)
8、GO学习之 函数(Function)
9、GO学习之 接口(Interface)
10、GO学习之 网络通信(Net/Http)
11、GO学习之 微框架(Gin)
12、GO学习之 数据库(mysql)
13、GO学习之 数据库(Redis)
14、GO学习之 搜索引擎(ElasticSearch)
15、GO学习之 消息队列(Kafka)
16、GO学习之 远程过程调用(RPC)
17、GO学习之 goroutine的调度原理
18、GO学习之 通道(nil Channel妙用)
文章目录
- GO系列
- 前言
- 一、nil channel读写阻塞
- 二、nil channel 妙用
- 2.1 一个普通示例
- 2.2 示例运行分析
- 2.3 如何妙用 nil channel
- 三、总结
前言
按照公司目前的任务,go 学习是必经之路了,虽然行业卷,不过技多不压身,依旧努力!!!
在《GO学习之 通道(Channel)》篇中,主要对 Channel 做了简介和用法,主要包含 无缓存通道、有缓存通道 和 单向通道 等,此篇补充一个 nil channel的用法,
看 nil channel 的妙用。
—— 本文内容借鉴《Go语音精进之路》一书。
一、nil channel读写阻塞
对于没有初始化的 channel (nil channel) 进行读写操作会发生阻塞,比如:
package main
func main() {
var c chan int
c <- 1
}
或者
package main
func main() {
var c chan int
<-c
}
无论是上哪段代码,运行都会得到错误的结果, main goroutine 被阻塞在 channel 上,导致 Go 运行时认为出现 deadlock状态并抛出 panic。
PS D:\workspaceGo\src\channel> go run .\nilChannel.go
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send (nil chan)]:
main.main()
D:/workspaceGo/src/channel/nilChannel.go:5 +0x25
exit status 2
二、nil channel 妙用
2.1 一个普通示例
一般我们在程序中习惯用 channel、goroutine 和 select 来搭配运行,例如:
下面的示例代码中,声明了 c1 c2 两个 channel,并且启动两个 goroutine 向 c1 c2 中延时后添加值,在 for 循环中用 select case 获取到 c1 c2 中的值。
package main
import (
"fmt"
"time"
)
func main() {
// 声明 c1 c2 两个通道,并且用 make 函数实例化
c1, c2 := make(chan int), make(chan int)
go func() {
time.Sleep(time.Second * 5)
c1 <- 5
close(c1)
}()
go func() {
time.Sleep(time.Second * 7)
c2 <- 7
close(c2)
}()
var ok1, ok2 bool
for {
select {
case x := <-c1:
ok1 = true
fmt.Println(x)
case x := <-c2:
ok2 = true
fmt.Println(x)
}
if ok1 && ok2 {
break
}
}
fmt.Println("program end!")
}
在上面的示例中,我们期望程序在接受完 c1 和 c2 两个 channel 上的数据后就退出,但实际运行结果如下:
PS D:\workspaceGo\src\channel> go run .\nilChannel.go
5
0
0
0
...
7
program end!
期望是程序在输出 5 和 7 之后退出,但实际结果中,输出完 5 之后,程序多输出了几个 0 之后才退出。
2.2 示例运行分析
- 程序开始运行,前 5s,
select
一直处于阻塞状态。 - 第 5s,c1 返回一个 5 后被关闭了,
select
语句的case x := <- c1
分支被执行,程序输出 5,则 for 循环开始新的 select 执行。 - c1 已经被关闭,由于从一个已经的关闭的 channel 接受数据将永远不会被阻塞,所以新一轮
select
又将case x := <- c1
被执行,由于 c1 是关闭状态的,从这个 channel 获取到对于类型的零值,即 0,于是程序输出了 0。所以在 for 循环里面,则一直输出 0 值。 - 2s 后,c2 被写入一个数值 7,此时在某一轮循环中,
select
则选出case x: <- c2
并执行。程序输出 7 之后满足条件退出循环,程序终止。
2.3 如何妙用 nil channel
nil channel并非一无是处,nil channel 只要用对了地方,用对了时候,就可以达到事半功倍的效果,将上面的实例程序做了改进,示例代码如下:
package main
import (
"fmt"
"time"
)
func main() {
// 声明 c1 c2 两个通道,并且用 make 函数实例化
c1, c2 := make(chan int), make(chan int)
go func() {
time.Sleep(time.Second * 5)
c1 <- 5
close(c1)
}()
go func() {
time.Sleep(time.Second * 7)
c2 <- 7
close(c2)
}()
for {
select {
case x, ok := <-c1:
//判断是否获取成功,不成功则把 c1 置为 nil
if !ok {
c1 = nil
} else {
fmt.Println(x)
}
case x, ok := <-c2:
//判断是否获取成功,不成功则把 c2 置为 nil
if !ok {
c2 = nil
} else {
fmt.Println(x)
}
}
if c1 == nil && c2 == nil {
break
}
}
fmt.Println("program end!")
}
程序的关键变化在判断 c1 或者 c2被关闭后,显示地把 c1 c2 两个 channel 置为了 nil。
有什么效果呢? 因为 对一个 nil channel 执行获取操作,该操作会被阻塞 ,因此已经被置为 nil 的 c1 c2 再也不会被 select 选中执行了。
运行结果:
PS D:\workspaceGo\src\channel> go run .\nilChannel.go
5
7
program end!
三、总结
此篇主要在特殊场景下,用了 nil channel 来巧妙的终结了 for select 程序,避免了运行结果的错误,因为 对一个 nil channel 执行获取操作,该操作会被阻塞,但对 一个一个关闭了的 channel 执行获取操作,则会得到 0值。