Go从入门到精通——在多个 goroutine 间通信的管道——通道(Channel)
在多个 goroutine 间通信的管道——通道(Channel)
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竟态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go 语言提倡使用通信的方法替代共享内存,这里通信的方法就是使用通道(channel)。
一、通道的特性
Go 语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道通信。
二、声明通道类型
通道本身需要一个类型进行修饰,就像切片类型需要标识元素类型。通道的元素类型就是在其内部传输的数据类型,声明如下:
var 通道变量 chan 通道类型
- 通道变量:保存通道的变量。
- 通道类型:通道内的数据类型。
chan 类型的空值是 nil,声明后需要配合 make 后才能使用。
三、创建通道
通道是引用类型,需要使用 make 进行创建,方式如下:
通道实例 := make(chan 数据类型)
- 数据类型:通道内传输的元素类型。
- 通道实例:通过 make 创建的通道句柄。
例如:
ch1 := make{chan int} //创建一个整型类型的通道
ch2 := make(chan interface) //创建一个空接口类型的通道,可以存放任意格式
type Equip struc{ /* 一些字母 */ }
ch2 := make(chan * Equip) //创建 Equip 指针类型的通道,可以存放 *Equip
四、使用通道发送数据
通道创建后,就可以使用通道进行发送和接收操作。
1、通道发送数据的格式
通道发送使用特殊的操作符 "<-",将数据通过通道发送的方式:
通道变量 <- 值
- 通道变量:通过 make 创建好的通道实例。
- 值:可以是变量、常量、表达式或者函数返回值等。值的类型必须与 ch 通道的元素类型一致。
2、通过通道发送数据的例子
使用 make 创建一个通道后,就可以是用 "<-" 向通道发送数据:
//创建一个空接口通道
ch := make(chan interface{})
// 将 0 放入通道中
ch <- 0
//将 hello 字符串放入通道中
ch <- "hello"
3、发送将持续阻塞直到数据被接受
把数往通道发送时,如果接收方一值都没有接收,那么发送操作将持续阻塞。
Go程序运行时能智能地发现一些永远无法发送成功的语句并做出提示,代码如下:
package main func main() { //创建一个整型通道 ch := make(chan int) //尝试将 0 通过通道发送 ch <- 0
五、使用通道接收数据
通道接收同样使用 "<-" 操作符,通道接收有如下特性:
- 通道的首发操作在不同的两个 goroutine 操作。
- 接收将要持续阻塞直到发送方发送数据。
- 每次接收一个元素。
5.1 阻塞接受数据
阻塞模式接收数据时,将接收变量作为 "<-" 操作符的左值,如下:
data := <-ch
执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。
5.2 非阻塞接收数据
使用非阻塞方式从通道接收数据时,语句不会发生阻塞,如下:
data, ok := <-ch
- data:表示接收到的数据。未接收到数据时,data 为通道类型的零值。
- ok:表示是否接收到数据。
5.3 接收任意数据,忽略接收到的数据
阻塞接收数据后,忽略从通道返回的数据,如下:
<-ch
执行该语句时,将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在 goroutine 间阻塞收发实现并发同步。
package main import ( "fmt" "time" ) func main() { //构建一个同步的通道 ch := make(chan int) //开启一个并发匿名函数 go func() { fmt.Println("start goroutine ...") time.Sleep(time.Duration(20) * time.Second) fmt.Println("wait 20 seconds") //匿名函数即将结束时,通过通道通知 main 的 goroutine,这一句会一直阻塞直到 main 的 goroutine 接收为止。 ch <- 0 fmt.Println("exit, goroutine") }() fmt.Println("wait for goroutine to finish ...") time.Sleep(time.Duration(20) * time.Second) fmt.Println("wait 20 seconds") //开启goroutine后,马上通过管道等待匿名 goroutine 结束。 <-ch time.Sleep(time.Duration(20) * time.Second) fmt.Println("wait 20 seconds") fmt.Println("all goroutine finished ...") }
程序输出如下:
Starting: D:\go-testfiles\bin\dlv.exe dap --check-go-version=false --listen=127.0.0.1:57829 from d:\go-testfiles DAP server listening at: 127.0.0.1:57829 Type 'dlv help' for list of commands. wait for goroutine to finish ... start goroutine ... wait 20 seconds exit, goroutine wait 20 seconds all goroutine finished ... Process 14368 has exited with status 0 Detaching dlv dap (1536) exited with code: 0
如果不添加 wait 20 秒,执行结果如下:
Starting: D:\go-testfiles\bin\dlv.exe dap --check-go-version=false --listen=127.0.0.1:57966 from d:\go-testfiles DAP server listening at: 127.0.0.1:57966 Type 'dlv help' for list of commands. wait for goroutine to finish ... start goroutine ... exit, goroutine all goroutine finished ... Process 19768 has exited with status 0 Detaching dlv dap (4112) exited with code: 0
注意:
- 首先会打印 fmt.Println("wait for goroutine to finish ...")
- 然后 main 会一直阻塞等待 goroutine 发送数据。同时执行 fmt.Println("start goroutine ...")
- ch <- 0 ,然后 <- ch
5.3 循环接收
通道的数据接收可以借用 for range 语句进行多个元素的接收操作,如下:
for data := range ch{ }
通道 ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过 for 遍历获得的变量只有一个,即上面例子中国的 data。
package main import ( "fmt" "time" ) func main() { //构建一个通道 ch := make(chan int) //开启一个并发匿名函数 go func() { //从 3 循环到 0 for i := 3; i >= 0; i-- { //发送 3 到 0 之间的数值 ch <- i //每次发送完时等待 time.Sleep(time.Second) } }() //遍历接收通道数据 for data := range ch { //打印通道数据 fmt.Println(data) //当遇到数据为 0 时,退出接收玄幻 if data == 0 { break } } }