Go语言 Channel通道详解
作者:勤天
一、通道介绍
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
go提倡使用通信的方法代替共享内存,这里通信的方法就是使用通道(channel),如下图所示。
在地铁站、食堂、洗手间等公共场所人很多的情况下,大家养成了排队的习惯,目的也是避免拥挤、插队导致的低效的资源使用和交换过程。代码与数据也是如此,多个 goroutine 为了争抢数据,势必造成执行的低效率,使用队列的方式是最高效的,channel 就是一种队列一样的结构。
Go 语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。
通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
1、声明通道
var 变量 chan 元素类型 var ch1 chan int // 声明一个传递整型的通道 var ch2 chan bool // 声明一个传递布尔型的通道 var ch3 chan []int // 声明一个传递int切片的通道
chan 类型的空值是 nil,声明后需要配合 make 后才能使用。
所以通道只能传输一种类型的数据,比如 chan int 或者 chan string,所有的类型都可以用于通道,空接口 interface{} 也可以。甚至可以(有时非常有用)创建通道的通道。
2、创建通道
通道是引用类型,需要使用 make 进行创建(分配内存),格式如下:
var ch1 chan string ch1 = make(chan string) //或者使用短类型 ch1 := make(chan string)
示例
ch1 := make(chan int) //创建一个整型类型的通道 ch2 := make(chan interface{}) //创建一个空接口类型的通道, 可以存放任意格式 type Equip struct{ /* 一些字段 */ } ch2 := make(chan *Equip) //创建Equip指针类型的通道, 可以存放*Equip
二、channel操作
通道有发送(send)、接收(receive)和关闭(close)三种操作。发送和接收都使用<-符号。
通道创建后,就可以使用通道进行发送和接收操作。
定义一个通道:
ch := make(chan int)
1、发送
将一个值发送到通道中。
ch <- 10 // 把10发送到ch中
2、接收
从一个通道中接收值。
x := <- ch // 从ch中接收值并赋值给变量x <-ch // 从ch中接收值,忽略结果
3、关闭
我们通过调用内置的close函数来关闭通道
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道
通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。(如果通道中还有数据的话)
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
三、无缓冲通道
无缓冲的通道又称为阻塞的通道
func main() { ch := make(chan int) ch <- 10 fmt.Println("发送成功") } //这段代码仅作为 描述无缓冲通道,实际会形成deadlock //具体原因,看下述分析
上面这段代码能够通过编译,但是执行的时候会出现以下错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
main.go:8 +0x54
上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?
一种方法是启用一个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("发送成功") }
因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。
无缓冲通道总结:
1)无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。
2)相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。
3)使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。
四、有缓冲的通道
解决上面问题的方法还有一种就是使用有缓冲区的通道。
我们可以在使用make函数初始化通道的时候为其指定通道的容量
1、有缓冲通道声明
通道实例 := make(chan 通道类型, 缓冲大小) func main() { ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道 ch <- 10 fmt.Println("发送成功") }
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。
2、阻塞条件
带缓冲通道在很多特性上和无缓冲通道是类似的。无缓冲通道可以看作是长度永远为 0 的带缓冲通道。因此根据这个特性,带缓冲通道在下面列举的情况下依然会发生阻塞:
- 带缓冲通道被填满时,尝试再次发送数据时发生阻塞。
- 带缓冲通道为空时,尝试接收数据时发生阻塞。
为什么对通道要限制长度而不提供无限长度的通道?
我们知道通道(channel)是在两个 goroutine 间通信的桥梁。使用 goroutine 的代码必然有一方提供数据,一方消费数据。当提供数据一方的数据供给速度大于消费方的数据处理速度时,如果通道不限制长度,那么内存将不断膨胀直到应用崩溃。
因此,限制通道的长度有利于约束数据提供方的供给速度,供给数据量必须在消费方处理量+通道长度的范围内,才能正常地处理数据。
五、循环读取信道
上面的代码一个一个地去读取信道简直太费事了,Go语言允许我们使用range来读取信道:
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 for v := range ch { fmt.Println(v) } } //deadline
如果你执行了上面的代码,会报死锁错误的,原因是range不等到信道关闭是不会结束读取的。也就是如果 缓冲信道干涸了,那么range就会阻塞当前goroutine, 所以死锁咯。那么,我们试着避免这种情况,比较容易想到的是读到信道为空的时候就结束读取
ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 for v := range ch { fmt.Println(v) if len(ch) <= 0 { // 如果现有数据量为0,跳出循环 break } }
以上的方法是可以正常输出的,但是注意检查信道大小的方法不能在信道存取都在发生的时候用于取出所有数据,这个例子 是因为我们只在ch中存了数据,现在一个一个往外取,信道大小是递减的。另一个方式是显式地关闭信道:
ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 // 显式地关闭信道 close(ch) for v := range ch { fmt.Println(v) }
被关闭的信道会禁止数据流入, 是只读的。我们仍然可以从关闭的信道中取出数据,但是不能再写入数据了。
六、关闭通道
可以通过内置的close()函数关闭channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)
package main import "fmt" func main() { c := make(chan int) go func() { for i := 0; i < 5; i++ { c <- i } close(c) }() for { if data, ok := <-c; ok { fmt.Println(data) } else { break } } fmt.Println("main结束") }
判断通道是否关闭?
当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。
当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢?
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 { // 通道关闭后再取值ok=false i, ok := <-ch1 if !ok { break } ch2 <- i * i } close(ch2) }() // 在主goroutine中从ch2中接收值打印 for i := range ch2 { // 通道关闭后会退出for range循环 fmt.Println(i) } }
说明:在知道通道的一些阻塞情况后,为了防止deadlock ,可以使用更友好的方式从通道中读取数据
if i, ok := <-ch1 ;ok{ ... }
七、单向通道
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
var 通道实例 chan<- 元素类型 // 只能发送通道 var 通道实例 <-chan 元素类型 // 只能接收通道
//往通道中写 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) }
总结
到此这篇关于Go语言 Channel通道详解的文章就介绍到这了,更多相关Go Channel通道内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!