Go channel使用注意事项
原文链接:https://www.zhoubotong.site/post/58.html
网上关于channel的使用有很多介绍,这里不在阐述,这里主要是记录下开发中,可能存在使用channel不当造成的问题总结下,
说道这里,还是总结下channel的几个特性吧:
- 给一个 空 channel发送数据,会造成永远阻塞
- 从一个 空 channel接收数据,会造成永远阻塞
- 给一个已经关闭的channel发送数据,会引起 panic
- 从一个已经关闭的channel接收数据,如果缓冲区中为空,则返回一个零值
- 无缓冲的channel是同步的,有缓冲的channel是异步的
Channel定义
var chn chan int // 声明一个传递类型为int的管道
var m map[string]chan bool // 声明一个map,元素是bool型的channel
chn := make(chan int) // 定义语法,定义需要使用内置函数make()即可,下面这行代码是声明+定义一个整型管道
chn := make(chan int, 10) // 事先定义好管道的size,下面这行代码定义管道的size为10
// 由管道中读写数据,<-操作符是与最左边的chan优先结合的
// 向管道中写入一个数据,在此需要注意:向管道中写入数据通常会导致程序阻塞,直到有
// 其他goroutine从这个管道中读取数据
chn <- value
// 读取数据,注意:如果管道中没有数据,那么从管道中读取数据会导致程序阻塞,直到有数据
value := <-chn
// 单向管道
var chn1 chan<- float64 // 只能向里面写入float64的数据,不能读取
var chn2 <-chan int // 只能读取int型数据,注意chan在箭头的右边,且chan 后跟了类型,一定是只读channel,如本处chn2
// 关闭channel,直接调用close()即可
close(ch)
// 判断ch是否关闭,判断ok的值,如果是false,则说明已经关闭(关闭的话读取是不会阻塞的)
v, ok := <-chn
废话不多说,大家看下下面的代码是如何执行的,输出结果是什么?
package main
import "fmt"
func main() {
var myChan chan int
myChan = make(chan int, 10) // 定义长度为10的channel
for i := 0; i < 10; i++ {
myChan <- i // 写入myChan,写10个值
}
close(myChan) //如果不关闭则会deadlock,注释掉本行看看结果输出什么?
for v := range myChan {
fmt.Println(v)
}
}
上面在没有注释close的情况下正常输出。否则fatal error: all goroutines are asleep - deadlock!。通过本例子我们知道了:
channel 在使用 for–range 的方式进行遍历的时候,需注意两个细节:
1:在遍历时,如果 channel 没有关闭,则会出现 deadlock 的错误,因此遍历前需要关闭channel
2:在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
我们接着继续看下面的例子:
package main
import (
"fmt"
"time"
)
func main() {
chn := make(chan int, 1000) //定义一个1000容量的channel
go func() {
for i := 0; i < 10; i++ { // 向管道chn发送写入10个数字
chn <- i
}
}()
go func() { // 开启一个携程 循环从chn中获取值
for {
i, ok := <-chn
if !ok { // 取值为空或零
fmt.Println("chn已经关闭")
return
}
fmt.Println("i = ", i)
}
}()
close(chn) // 这里直接关闭了chn
fmt.Println("sucess")
time.Sleep(time.Minute * 1) // 等待1分钟让携程全部执行完毕
}
上面代码最大的问题在哪里? 很明显。代码中开启了两个go携程,第一个写数据,第二个读取数据,开完两个goroutine之后main主携程直接close掉channel,
然后直接输出打印:success。
但是往已经关闭的channel写入数据会panic的。所以上面的结果输出:
sucess
chn已经关闭
panic: send on closed channel
goroutine 6 [running]:
main.main.func1()
/media/uos/G/web/demo-go/main.go:12 +0x36
created by main.main
/media/uos/G/web/demo-go/main.go:10 +0x6b
exit status 2
通过上面在2个携程中抛出panic来看,咋们如何解决协程中出现这种 panic导致程序崩溃问题?其实在
goroutine 中使用 recover,就可以解决协程中出现 panic。下面我起了一个协程,但是这个协程出现了panic,如果没有甫获这个panic,
就会造成整个程序崩溃,这时可以在goroutine中使用ecover来捕获panic,进行处理,这样即使这个协程发生的问题,
但是主线程仍然不受影响,可以继续执行。
package main
import (
"fmt"
"time"
)
func sayTest() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("here is a test")
}
}
// 函数
func catchPanic() {
//这里我们可以使用defer + recover
defer func() {
//捕获catchPanic抛出的panic
if err := recover(); err != nil {
fmt.Println("catchPanic() 发生了错误", err)
}
}()
var myMap map[string]string //定义了一个map
myMap["code"] = "golang" //此行error 因为没有make初始化分配内存地址
}
func main() {
// 开启两个携程
go sayTest()
go catchPanic()
time.Sleep(time.Second * 2) // 没耐心等待携程执行完毕,提前看看结果,毕竟sayTest sleep了10s
}
输出:
catchPanic() 发生了错误 assignment to entry in nil map
here is a test
here is a test
在介绍一个使用select 解决从管道取数据的阻塞问题:
package main
import (
"fmt"
)
// 使用select解决从管道取数据的阻塞问题
func main() {
intChn := make(chan int, 10) // 定义一个管道10个数据int
for i := 0; i < 10; i++ {
intChn <- i // 往intChn写入10个数字
}
strChn := make(chan string, 5) // 定义一个管道 5个数据string
for i := 0; i < 5; i++ {
strChn <- "我是Gofans" + fmt.Sprintf("%d", i) // Sprintf返回的string,字符串可以直接连接
}
//使用传统的方法在遍历管道时,如果不关闭会阻塞而导致deadlock,上面的示例中有讲到这块
//问题,在实际开发中,可能我们不好确定什么时候关闭该管道
//可以使用select的方式可以解决
for {
select {
//注意这里如果intChn一直没有关闭,不会一直阻塞而deadlock
//case会自动到下一个case匹配
case v := <-intChn:
fmt.Printf("从initChn读取的数据%d\n", v)
case v := <-strChn:
fmt.Printf("从strChn读取的数据%s\n", v)
default:
fmt.Printf("两个channel都取不到数据了,到此结束吧\n")
return
}
}
}
上面就介绍这么多了,欢迎广大老铁留言补充。