40、Go语言基础 - 并发编程 - channel(通道)
hi,我是温新
channel 概述
**在 Go 语言中,channel 是协程之间的通信机制。**一个 channel 是一条通信的管道,它可以让一个协程通过 channel 给另一个协程发送数据。每个 channel 都需要指定数据类型,即 channel 可发送数据的类型。如果使用 channel 发送 int 类型数据,可以写成 chan int
。数据发送的方式如同水在管道中的流动。
在 Go 语言中,提倡使用 channel 的方式替代共享内存来进行协程间的数据共享。换而言之,Go 主张通过数据传递来实现共享内存,而不是通过共享内存来实现数据传递。
Go 语言中的**通道(channel)是一种特殊的类型。**通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
创建 channel 类型
声明通道
声明 channel 类型的语法格式如下:
var 通道变量 chan 元素类型
通道变量
:保存通道的变量;
元素类型
:通道内的数据类型。
chan
类型的空值是 nil,声明后需要配合 make() 才能使用。
案例:
package main
import "fmt"
func main() {
// 声明通道
var ch chan int
fmt.Println(ch) // nil
// 使用 make 初始化
ch = make(chan int, 1)
fmt.Println(ch) // 0xc000070120
}
创建通道
通道是引用类型,需要使用 make 进行创建,格式如下:
通道实例 := make(chan 数据类型, [缓冲区大小])
通道实例
:通过 make 创建的通道句柄;
数据类型
:通道内传输的元素类型。
案例:
package main
import "fmt"
func main() {
ch := make(chan int)
fmt.Println(ch) // 0xc000070120
}
操作 channel
通道创建后,可以使用通道进行发送和接收操作。
通道发送数据
通道发送使用特殊的操作符<-
,将数据通过通道发送的格式如下:
# 含义:把 值 赋值给 通道变量
通道变量 <- 值
通道变量
:通过 make
创建好的通道实例;
值
:可以是变量、常量、表达式或函数返回值等。值的类型必须与 ch 通道的元素类型一致。
使用通关接收数据
通道接收数据同样使用 <-
符号,通道接收有如下特性:
- 1、通道的收发操作在不同的两个 goruntine 中进行;
- 由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另一个 goruntine 中进行。
- 2、接收将持续阻塞知道发送方发送数据;
- 若接收方接收时,通道中没有发送方发送的数据,接收方也会发生阻塞,知道发送方发送数据为止。
- 3、每次接收一个元素。
- 通道一次只能接收一个数据元素。
通道的数据接收有如下 4 种方式:
方式 1:阻塞接收数据
data := <- ch
# 含义:从通道 ch 中取出数据并赋值给 data
该方式接收会阻塞,直到接收到数据并赋值给 data 变量。
方式 2:非阻塞接收数据
data, ok := <- ch
data
:表示收到的数据。未接收到数据时,data 为通道类型的 零值。
ok
:表示是否接收到数据。
非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。若需要实现接收超时检测,可以配合 select 和 计时器channel 进行。
方式 3:接收任意数据,忽略接收的数据
阻塞接收数据后,忽略从通道返回的数据,其格式如下:
<- ch
该方式将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。其目的不在于接收 channel 中的数据,而是为了阻塞 goruntine。
方式 4:循环接收
使用 for range
循环接收,格式如下:
for data := range ch {
}
通道 ch 是可以遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。
使用 channel 时需要考虑发生死锁(deadlock)的可能。如果 goruntine 在一个 channel 上发送数据,其他的 channel 应用接收得到的数据;如果没有接收,那么程序将运行时出现死锁。如果 goruntine 正在等待从 channel 接收数据,其他一些 goruntine 将会在该 channel 上写入数据;若没有写入,程序将会死锁。
接收数据的案例
基本案例
package main
import "fmt"
func main() {
ch := make(chan int, 1)
// 向 ch 通道中发送数据
ch <- 10
// 从通道中接收数据
data := <-ch
fmt.Println(data)
// 关闭通道
close(ch)
}
方式 1:阻塞接收
package main
import "fmt"
func main() {
ch1 := make(chan string)
go sendData(ch1)
// 接收数据:方式 1
for {
data := <-ch1
// 通道关闭,通道中传输的数据则为和数据类型的默认值
// chan int 默认值为 0,
// chan string 默认值 空
// 等于空,数据传输完毕,中断循环执行
if data == "" {
break
}
fmt.Println("从通道中读取数据的方式 1:", data)
}
// 方式 2
// 方式 3
// 方式 4
}
func sendData(ch1 chan string) {
// 关闭通道
defer close(ch1)
for i := 0; i < 3; i++ {
ch1 <- fmt.Sprintf("发送数据%d\n", i)
}
fmt.Println("数据发送完毕")
}
输出结果
从通道中读取数据的方式 1: 发送数据0
从通道中读取数据的方式 1: 发送数据1
从通道中读取数据的方式 1: 发送数据2
在这个案例中,sendData
函数中发送发 3 次数据;而通道一次只能取出一条数据,因此使用循环来读取全部数据。
下面的方式2、3、4 都将在 main 函数中执行,因此不贴出所有代码了。
**方式 2:非阻塞接收 **
for {
data, ok := <-ch1
fmt.Println(ok)
// 通过返回值形式来判断通道是否关闭
// 若通道关闭,则 ok 值返回 false
// 通道关闭,则中断循环
if !ok {
break
}
fmt.Println("从通道中读取数据的方式 2:", data)
}
输出结果
true
从通道中读取数据的方式 2: 发送数据1
true
从通道中读取数据的方式 2: 发送数据2
数据发送完毕
false
方式 3:for range 接收
for data := range ch1 {
fmt.Println("从通道中读取数据的方式 4:", data)
}
缓冲 channel
默认创建的都是非缓冲 channel,读写都是即时阻塞。
缓冲 channel 自带一块缓冲区,可以暂时存储数据,若缓冲区满了,就会发生阻塞。
无缓冲的 channel
无缓冲的通道又称为阻塞的通道。
package main
import "fmt"
func main() {
ch := make(chan string)
ch <- "王美丽" // 这里形成死锁
fmt.Println("send succ")
}
该代码可以通过编译,但是会在运行时发生错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
deadlock
即死锁。表示这个程序中的 goruntine 被挂起导致程序死锁。出现死锁的原因如下:
使用 ch := make(chan string)
创建的是无缓冲的通道。无缓冲的 channel 只有在接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。
解决之道,创建一个 goruntine 去接收通道的数据,如下:
package main
import (
"fmt"
)
func main() {
ch := make(chan string)
// 创建一个通道接收数据
go recv(ch)
// 向通道中写入数据
ch <- "王美丽"
fmt.Println("send succ")
}
func recv(ch chan string) {
// 接收通道中的数据
res := <-ch
fmt.Println("数据接收成功,名字是:", res)
}
有缓冲的 channel
解决死锁的另一种方法是使用缓冲区,可以在使用 make 时进行初始化。
package main
import "fmt"
func main() {
ch := make(chan string, 1)
ch <- "王美丽"
fmt.Println("发送成功")
}
单向 channel
channel 默认都是双向的,即可读可写。定向 channel 也叫单向 channel,只读或只写。
只读 channel 使用格式如下:
make(<- chan Type)
<- chan
只写 channel 使用格式如下:
make(chan <- Type)
chan <- data
案例
package main
import (
"fmt"
"time"
)
func main() {
// 双向 channel
ch1 := make(chan string)
go func1(ch1)
data := <-ch1
fmt.Println("main 函数接收到的数据:", data)
ch1 <- "王美丽在学习 Go"
ch1 <- "hao"
go func2(ch1)
go func3(ch1)
time.Sleep(1 * time.Second)
fmt.Println("succ")
}
func func1(ch1 chan string) {
ch1 <- "我是王美丽"
data := <-ch1
data2 := <-ch1
fmt.Println("回应:", data, data2)
}
// 只写入数据
func func2(ch1 chan<- string) {
// 只能写入
ch1 <- "你好鸭,王美丽"
}
// 只读数据
func func3(ch1 <-chan string) {
data := <-ch1
fmt.Println("只读:", data)
//ch1 <- "hello" // Invalid operation: ch1 <- "hello" (send to the receive-only type <-chan string)
}
输出结果:
main 函数接收到的数据: 我是王美丽
回应: 王美丽在学习 Go hao
只读: 你好鸭,王美丽
succ
操作/状态 | nil | 无值 | 有值 | 满 |
---|---|---|---|---|
发送 | nil | 发送成功 | 发送成功 | 阻塞 |
接收 | 阻塞 | 阻塞 | 接收成功 | 接收成功 |
关闭 | panic | 关闭成功 | 关闭成功 | 关闭成功 |
select 多路复用
有些时候需要从多个 channel 中接收数据,channel 在接收数据时,若没有数据可以被接收,那么当前 goruntine 将会发生阻塞。Go 中内置了关键字 select
用于响应多个 channel 的操作。
select 类似于 switch,也有 case 分支和默认分支。每个 case 分支对应一个 channel 的通信过程。select 会一直等待,知道其中的某个 case 的通信操作完成时,就会执行 case 分支对应的代码,其使用格式如下:
select {
case <-ch1:
//
case data := <-ch2:
//
case ch3 <- 10:
//
default:
// 默认操作
}
select 语句的特点如下:
- 1、可以处理一个或多个 channel 的发送/接收操作;
- 2、若有多个 case 同时满足,select 会随机选择一个执行;
- 3、对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。
package main
func main() {
ch := make(chan int, 1)
go func(chan int) {
for {
select {
// 0或者1写入是随机的
case ch <- 0:
case ch <- 1:
}
}
}(ch)
for i := 0; i < 10; i++ {
println(<-ch)
}
}