Go 语言通道(channel)
上一节我 们介绍了协程(goroutine)的概念和用法,但是单纯地将函数并发执行是没有意义的,函数与函数间需要交换数据才能体现并发执行函数的意义。Go 语言为 goroutine 间的数据交换提供了一种简单的方法 —— 通道(channel)。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出的规则,保证收发数据的顺序。Go 的每一个通道都是一个具体类型的导管,因此声明 channel 的时候需要为其指定元素类型。同时,通道还提供了同步机制,确保数据在发送和接收之间的安全传递,可以有效地避免竞态条件和死锁等并发问题。
本文所有示例代码可在 GitHub 下载。
通道的创建
声明通道类型变量需要使用 chan 关键字,语法格式如下:
var 变量名 chan 元素类型
注意:声明通道时需要指定元素类型,即通道中传递的元素的类型。
例如:
var a chan int // 声明一个传递 int 类型数据的通道
var b chan string // 声明一个传递 string 类型数据的通道
var c chan bool // 声明一个传递 bool 类型数据的通道
初始化通道
未经初始化的通道默认值为 nil,需要使用 make() 函数初始化之后才能使用,语法格式如下:
make(chan 数据类型, [缓冲大小])
其中 channel 的缓冲大小是可选的。
例如:
ch := make(chan int) // 创建一个传递 int 类型数据的通道
ch := make(chan int) // 创建一个传递 int 类型数据、缓冲大小为10的通道
通道的操作
通道共有发送、接收、关闭三种操作,而发送和接收操作均用 <- 符号,请看下面几个例子。
-
声明通道并初始化
ch := make(chan int) // 声明一个通道并初始化 -
给一个通道发送值
ch <- 10 // 把10发送给ch通道 -
从一个通道中取值
x := <-ch // x从ch通道中取值<-ch // 从ch通道中取值,忽略结果 -
关闭通道
close(ch) // 关闭通道
注意:一个通道值是可以被垃圾回收掉的。
通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致 panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致 panic。
无缓冲的通道
无缓冲的通道又称为阻塞的通道,我们来看一下如下代码片段:
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 100
fmt.Println("发送成功")
}
上面这段代码能够通过编译,但是执行时会报错:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/home/rudy/workspace/go-courses/basic/goroutine/channel_example_01.go:7 +0x28
exit status 2
deadlock 表示我们程序中所有的 goroutine 都被挂起导致程序死锁了,为什么会出现这种情况呢?
这是因为我们创建的是一个无缓冲区的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。
我们可以创建一个 goroutine 去接收值,例如:
package main
import "fmt"
func receive(ch chan int) {
ret := <-ch
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go receive(ch)
ch <- 100
fmt.Println("发送成功")
}
以上代码执行结果如下:
接收成功 100
发送成功
有缓冲区的通道
另外还有一种方法解决上述死锁的问题,那就是使用有缓冲区的通 道。我们可以在使用 make 函数初始化通道时,为其指定缓冲区大小,例如:
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 100
fmt.Println("发送成功")
}
以上代码执行结果如下:
发送成功
只要通道的容量大于零,那么该通道就属于有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作。
你可以使用内置的 len() 函数获取通道的长度,使用 cap() 函数获取通道的容量。
判断通道关闭
当向通道中发送完数据时,我们可以通过 close() 函数来关闭通道。当一个通道被关闭后,再往该通道发送值会引发panic。从该通道取值的操作会先取完通道中的值。通道内的值被接收完后再对通道执行接收操作得到的值会一直都是对应元素类型的零值。那我们如何判断一个通道是否被关闭了呢?
value, ok := <-ch
其中,value 是从通道中所取得的值;ok 是状态,若通道已关闭,返回 false,否则返回 true。
以下代码会不断从通道中取值,直到通道被关闭后退出。
package main
import "fmt"
func receive(ch chan int) {
for {
v, ok := <-ch
if !ok {
fmt.Println("通道已关闭")
break
}
fmt.Printf("v:%#v ok:%#v\n", v, ok)
}
}
func main() {
ch := make(chan int, 1)
ch <- 1
close(ch)
receive(ch)
}
以上代码执行结果如下:
v:1 ok:true
通道已关闭