描述
- 主要用于多个goroutine间传递数据.一个通道相当于一个先进先出(FIFO)的队列.
- channel用来在协程[goroutine]之前传递数据,准确的说,是用来传递数据的所有权。
- 一个设计良好的程序应该确保同一时刻channel里面的数据只会被同一个协程拥有,这样就可以避免并发带来的数据不安全问题[data races]。
- 官方的go编译器限制channel最多能容纳到65535个元素,但我们不宜传递体积过大的元素值,因为channel的数据从进入到流出会涉及到数据拷贝操作。如果元素体积过大,最好的方法还是使用传递指针来取代传递值。
内部结构
每个channel内部实现都有三个队列。
- 接收消息的协程队列。
这个队列的结构是一个限定最大长度的链表,所有阻塞在channel的接收操作的协程都会被放在这个队列里。 - 发送消息的协程队列。
这个队列的结构也是一个限定最大长度的链表。所有阻塞在channel的发送操作的协程也都会被放在这个队列里。 - 环形数据缓冲队列。
这个环形数组的大小就是channel的容量。如果数组装满了,就表示channel满了,如果数组里一个值也没有,就表示channel是空的。 - 对于一个阻塞型channel来说,它总是同时处于即满又空的状态。一个channel被所有使用它的协程所引用,也就是说,只要这两个装了协程的队列长度大于零,那么这个channel就永远不会被垃圾回收。另外,协程本身如果阻塞在channel的读写操作上,这个协程也永远不会被垃圾回收。
分类
- 非缓冲通道
make的时候第二个参数为0或者不填.
无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。也就是说,只有收发双方对接上了,数据才会被传递.数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int) //非缓冲信道
go func() {
ch1 <- 1
fmt.Println("after 5 seconds, output this") //阻塞5秒后,主线程ch1信道才被接收,才输出该语句
}()
time.Sleep(5 * time.Second)
<-ch1
time.Sleep(2 * time.Second) //防止主线程结束,关闭其他线程
}
- 缓冲通道
make 的时候第一个参数大于0.
在有容量的时候,发送和接收是不会互相依赖的.用异步的方式传递数据。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int, 1) //缓冲信道
go func() {
ch1 <- 1
fmt.Println("not after 5 seconds, output this")
}()
time.Sleep(5 * time.Second)
fmt.Println(<-ch1)
}
特性
- 是类型安全的
- 发送操作之间是互斥的,接收操作之间也是互斥的(多线程安全)
- 进入通道的并不是在接收操作符右边的那个元素值,而是它的副本
- 移出通道的是通道元素的副本
- channel关闭后,如果还有数据还是可以正常读取的,等读取完后,v,ok := <- ch,其中ok是false,v是对应类型的零值
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int, 5) //缓冲信道
go func() {
for i := 0; i < 5; i++ {
ch1 <- i
if i >= 4 {
close(ch1)
}
}
}()
time.Sleep(5 * time.Second)//保证上个goroutine结束,即ch1是关闭的
i := 0
for {
v,ok := <-ch1
fmt.Println(v,ok)
i++
if i > 7 { //若不加这段代码,会一直读
break
}
}
}
输出:
0 true
1 true
2 true
3 true
4 true
0 false
0 false
0 false
- 通道会阻塞goroutine
- 关闭一个只读channel是非法的,编译器直接报错
- 使用 v, ok <- ch 接收一个值。第二个遍历ok是可选的,它表示channel是否已关闭。接收值只会又两种结果,要么成功要么阻塞,而永远也不会引发panic
- 不同于array/slice/map上的for-range,channel的for-range只允许有一个变量
package main
import (
"fmt"
)
func main() {
ch1 := make(chan int, 5) //缓冲信道
go func() {
for i := 0; i < 5; i++ {
ch1 <- i
if i >= 4 {
close(ch1) //如果不加这句,会因为range一直读一个未关闭channel,而死锁
}
}
}()
for v := range ch1 { //一直接收channel数据,直到channel关闭
fmt.Println(v)
}
}
阻塞的Case
- 通道容量已满情况,执行写入
- 通道没有数据,执行读取
- 对于值为nil的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行.
- 对于值为nil的通道,若程序只有一个goroutine,向其中发送数据或接收数据,都会阻塞,程序会死锁;若程序有多个goroutine,只会阻塞那个向nil通道发送或接收数据的goroutine,程序不会panic
panic的Case
- 关闭已经关闭的channel
- 已经关闭的channel发送数据
单向通道
make(chan<- int, 1)
make(<-chan int, 1)
- 作用
a.作为函数参数,从而约束函数体内的行为;
b.作为函数返回值,从而约束返回后的行为;
//函数返回一个单向通道,那么该通道只能接收,不能发送
func getIntChan() <-chan int {
ch := make(chan int, 5)
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
return ch
}
func main() {
ch := getIntChan()
ch <- 6 //编译报错send to receive-only type <-chan int
}
正常通道关闭
-
从channel的接收协程队列中移除所有的goroutine,并唤醒它们。
-
从channel的接收协程队列中移除所有的goroutine,并唤醒它们。
-
一个已关闭的channel内部的缓冲数组可能不是空的,没有接收的这些值会导致channel对象永远不会被垃圾回收。
总结
- 如果channel关闭了,那么它的接收和发送协程队列必然空了,但是它的缓冲数组可能还没有空。
- channel的接收协程队列和缓冲数组,同一个时间必然有一个是空的
- channel的缓冲数组如果未满,那么它的发送协程队列必然是空的
- 对于缓冲型channel,同一时间它的接收和发送协程队列,必然有一个是空的
- 对于非缓冲型channel,一般来说同一时间它的接收和发送协程队列,也必然有一个是空的,但是有一个例外,那就是当它的发送操作和接收操作在同一个select块里出现的时候,两个队列都不是空的。