Channel
Channel 管道,这是 Go 的并发世界里至关重要的一块,它允许不同 goroutines 之间的数据交换和同步。
- 管道中的数据流动方式与队列一样,即先进先出。
核心用法是下面两段:
go
make(chan 元素类型, 缓冲大小) // channel 的类型决定了他所能传递的数据类型,第二个参数是可选的,用于指定 channel 的缓冲大小。
// 例如:
make(chan int, 100) // 有缓冲的channel,可以暂存100个值,满了就阻塞发送方的goroutine。
make(chan string) // 无缓冲的channel,只能暂存一个值,满了就会阻塞发送方的goroutine。go
// 箭头方向即为数据流的方向,只有<- ,没有->
ch <- x // 发送数据
x = <-ch // 接收数据并赋值给x
<-ch // 接收数据并丢弃管道类型
就是 chan ,但直接声明 var ch chan int 这种是不能直接使用的,只有 make 初始化的管道才可以正常使用。
管道操作
创建
make 一般有三个参数,用于创建 slice、map、channel,但创建管道时最多只会用到两个参数。
go
intCh := make(chan int)
// 缓冲区大小为1的管道
strCh := make(chan string, 1)关闭
有一个内置函数:func close(c chan<- Type)
示例:
go
func main() {
intCh := make(chan int)
// do something
close(intCh)
}有时候使用 defer 关闭管道更好:
go
func main() {
// 创建有缓冲管道
ch := make(chan int, 5)
// 创建两个无缓冲管道
chW := make(chan struct{})
chR := make(chan struct{})
// 在 main 结束后关闭管道
defer func() {
close(ch)
close(chW)
close(chR)
}()
}读写
略一部分(上方有)
go
ints, ok := <-intCh依旧可使用ok判断是否读取成功。
无缓冲
部分略(上方有)
无缓冲管道不应该同步的使用,正确来说应该开启一个新的协程来发送数据:
go
func main() {
// 创建无缓冲管道
ch := make(chan int)
defer close(ch)
go func() {
// 写入数据
ch <- 123
}()
// 读取数据
n := <-ch
fmt.Println(n)
}有缓冲
尽管channel具备缓冲,但依旧非常危险,一旦缓冲满了,就会阻塞发送方,而缓冲空了则会阻塞接收方。
缓冲只是缓解短时突发的手段,不是同步或无限吞吐的解决方案。
因此在并发设计中必须谨慎使用并配合其他控制手段,比如增加接收方数量或提高单个接收方的处理能力、增加限流手段等等。
访问管道信息
用 len(ch) 可知正在缓冲的数据数量,用 cap(ch) 可知管道缓冲区大小。
go
func main() {
// 创建并发送数据
ch := make(chan int, 5)
ch <- 1
ch <- 2
ch <- 3
// 打印len和cap
fmt.Println(len(ch), cap(ch))
}
/* 输出
3 5
*/小技巧
等待子协程
不只有缓冲满了才能阻塞发送方,管道内没有数据也可以阻塞接收方。
利用这一条件,可实现主协程等待子协程执行完毕的场景。
go
func main() {
// 创建一个无缓冲管道
ch := make(chan struct{})
defer close(ch)
go func() {
fmt.Println(2)
time.Sleep(1 * time.Second)
// 写入数据
ch <- struct{}{}
}()
// 没有数据无法读取,阻塞,等待数据
<-ch
fmt.Println(1)
}使用range等待
range在go里面是一个通用的迭代器。
对于channel而言,range可以不断地接收管道里面的数据,同时会阻塞当前协程,直到管道被关闭。
比<-ch更优雅的写法:
go
// 更优雅的等待:range 会在 channel 被关闭后退出
for range ch {
// 不需要对 v 做任何处理——这里只是等待信号
}
// 如果要提取数据
for v := range ch {
fmt.Println(v)
}仅通知
若一个管道仅用于通知单个消息,可以传递struct{}{}空数据。
go
ch <- struct{}{}go
func main() {
defer func() {
close(ch)
close(chW)
close(chR)
}()
// 负责写
go func() {
for i := 0; i < 10; i++ {
ch <- i
fmt.Println("写入", i)
}
chW <- struct{}{}
}()
// 负责读
go func() {
for i := 0; i < 10; i++ {
// 每次读取数据都需要花费1毫秒
time.Sleep(time.Millisecond)
fmt.Println("读取", <-ch)
}
chR <- struct{}{}
}()
fmt.Println("写入完毕", <-chW)
fmt.Println("读取完毕", <-chR)
}一些注意点
- 管道最好要由发送方关闭,因为大多数情况下接收方只知道接收数据,并不知道该在什么时候关闭管道。
Select
select语句可以让goroutine同时等待多个通信操作。
select会阻塞,直到其中一个case可以运行,若多个case已就绪,他会随机选一个执行。
go
select {
case n, ok := <-chA:
fmt.Println(n, ok)
case n, ok := <-chB:
fmt.Println(n, ok)
case n, ok := <-chC:
fmt.Println(n, ok)
default:
fmt.Println("所有管道都不可用")
// 没有管道可用时,使用 default 分支可以使协程不阻塞(但也使程序跳过了接收数据的步骤。。)。
}