channel
单纯地将函数并发执行是没有意义的。函数与函数之间需要交换数据才能体现出并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中很容易发生竞态问题。为了保证数据交换的准确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes)提倡通过通信共享内存而不是通 过共享内存实现通信。
如果说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。
var ch chan int
print(ch) // nil
声明的通道后需要使用 make 函数 初始化后才能使用。
创建 channel 的格式如下:
make(chan 元素类型 , [缓冲大小])
channel 的缓冲大小是可选的。
例子:
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
channel操作
通道有发送(send)、接收(receive)和关闭(close)三种操作
发送和接收都是用 <- 符号
首先我们可以先定义一个通道:
ch := make(chan int)
将一个值发送到通道中:
ch <- 20 // 把20发送到通道中
从通道中接收值:
x := <- ch //从通道中接收值并赋值给 x
<- ch //从通道中接收值,忽略结果
关闭通道:
close(ch)
PS:只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道在发送值就会导致panic
- 对一个关闭的通道进行接收会一直获取值直到通道为空
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值
- 关闭一个已经关闭的通道会导致panic
无缓冲通道
无缓冲的通道又称为阻塞的通道。
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
上后面这段代码运行能够通过编译,但是执行会出现下面错误:
为什么会出现deadlock错误呢?
因为我们使用 ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有变量接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
上面的代码会阻塞在 ch <- 20 这一行代码形成死锁,如何解决这个问题呢?
一种办法是启用一个goroutine去接收值,例如:
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}
无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接受操作先执行,接收方的goroutine将继续阻塞,直到另一个goroutine在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道
有缓冲的通道
我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
例子:定义一个缓冲大小为 5 的通道,从结果中可以看出,当缓冲满的时候,发送操作会被阻塞,要想再写入必须要取出缓冲的数据。
func main() { c := make(chan int, 5) go func() { for i := 0; i < 10; i++ { c <- i println("in :", i) } close(c) }() for { if data, ok := <-c; ok { println("out :", data) } else { println("out :nil") break } } print("函数执行完毕") }
可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量
close()
可以通过内置的close() 函数关闭 channel (如果管道不往里存值或者取值的时候一定记得关闭管道)
func main() {
c := make(chan int, 1)
go func() {
for i := 0; i < 10; i++ {
c <- i
println("in :", i)
}
close(c)
}()
for {
if data, ok := <-c; ok {
println("out :", data)
} else {
break
}
}
print("函数执行完毕")
}
如何优雅的从通道循环取值
当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢?
// channel 练习
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)
}()
// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
go func() {
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中从ch2中接收值打印
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
}
从上面的例子中我们看到有两种方式在接收值的时候判断通道是否被关闭,我们通常使用的是for range的方式
单向通道
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
Go语言中提供了单向通道来处理这种情况。例如,我们把上面的例子改造如下:
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
1. chan<- int是一个只能发送的通道,可以发送但是不能接收(只能写入数据到通道);
2. <-chan int是一个只能接收的通道,可以接收但是不能发送(只能从通道取数据)。
在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。