Go channel使用注意事项
网上关于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!。通过本例子我们知道了:
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 } } }
上面就介绍这么多了,欢迎广大老铁留言补充。