一.介绍
单纯的将函数并发执行是没有意义的。函数和函数之间需交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存来进行数据交换,但是共享内存在不同的goroutine中容易发送竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP,提倡通过通信共享内存,而不是通过共享内存通信。
如果说goroutine是Go程序并发的执行体,channel就是他们之间的连接。channel是可以让一个goroutine发送特定值到另外一个goroutine的通信机制。
Go语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总遵循先入先出的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
二. channel类型
channel是一种类型,一种引用类型。声明通道类型的格式如下:
var 遍历 chan 元素类型
举几个例子:
var ch1 chan int //声明一个传递整型的通道
var ch2 chan bool //声明一个传递布尔型的通道
var ch3 chan []int //声明一个传递int切片的通道
三. 创建channel
通道是引用类型,通道类型的默认值是nil。
创建channel的格式如下:
make(chan 类型, 缓存大小)
channel的缓存大小是可选的。
举个例子:
ch1 := make(chan int)
ch2 := make(chan bool)
ch3 := make(chan, []int)
四. channel操作
通道有发送,接收核关闭三种操作。
发送和接收都使用<-符号。关闭channel我们通过内置的close函数。
关于关闭通道需要注意的是,只有在通知接收方goroutine所有数据都发送完毕时才需要关闭通道。通道可以被垃圾回收机制回收的,他和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道发送值会导致panic
- 对一个关闭的通道接收值会一直获取值直到通道为空
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的默认值
- 关闭一个已经关闭的通道会导致panic
五. 无缓冲的通道
创建无缓冲的通道:
make(chan 类型)
make(chan 类型, 0)
无缓冲的通道又被称作阻塞的通道。
为什么会出现deadlock错误呢?
因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。无缓冲的通道必须有接收才能发送。否则会造成一直阻塞,导致死锁。
解决:
无缓冲通道上的发送操作会阻塞(先发送再接收),直到另外一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,(先接收再发送)如果接收方操作先执行,接收方的goroutine将阻塞,直到另外一个goroutine在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称作同步通道。
六. 有缓冲的通道
创建有缓冲的通道:
make(chan 类型, 缓冲大小)
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放的数量,容量满了会发生阻塞,直到有别的协程取出元素。
我们可以使用内置的len函数获取通道中元素的数量,使用cap函数获取通道的容量。虽然我们很少这么做。
七. close
可以通过内置的close()函数关闭channel,如果你不往管道里存值或取值的时候一定记得关闭管道。
八. 如何优雅的从通道中循环取值
当通过通道发送有限数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。
当通道被关闭时,往该通道发送值会引发panic。当通道里没有数据时,从该通道里接收的值一直都是类型默认值。
如何判断一个通道是否被关闭了呢?
注意:对一个关闭的通道接收值会一直获取值直到通道为空,并且没有值的通道执行接收操作会得到对应类型的默认值。所以,我们需要判断一个通道是否关闭了。
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
//开启一个goroutine将0~100的数据发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1) //发送完数据之后,关闭ch1
}()
//开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
go func() {
for {
if v, ok := <-ch1; ok { //通道关闭后再取值 ok=false
ch2 <- v * v
} else {
break
}
}
close(ch2) //发送完数据之后,关闭ch2
}()
//在主协程中从ch2中接收值
for data := range ch2 { //通道关闭会退出for range循环
fmt.Println(data)
}
}
从上面的例子可以看出,有两种方式在接收值的时候判断通道是否关闭。
if data, ok := <- ch; ok{//通道关闭 ok=false
}
//常用
for data := range ch{//通道关闭,退出range
}
九. 单向通道
有的时候我们将通道作为参数在多个任务函数之间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或接收。
Go语言提供了单向通道来处理这种情况。
其中:
- chan <- int是一个只能发送的通道,可以发送但是不能接收。即可以放值到该通道中,发送是相对于当前协程来说的。
- <- chan int是一个只能接收的通道,可以接收但是不能发送。即可以从该通道中获取值,接收是相对于当前协程来说的。
在函数传参及任何赋值操作中,将双向通道转换为单向通道是可以的,但是反过来不可以。
十. 总结
channel常见的异常总结:
注意:关闭已经关闭的channel也会产生panic。