Channels是一个管道,Go中写作chan,数据沿箭头方向流动,通道必须在使用前make创建,示例代码是对一个切片中的数字求和,将工作分配给两个goroutine。 一旦两个goroutine都完成了它们的计算,它就会计算出最终的结果
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
1. 创建
创建能存储3个 int 类型数据的chan
ch:=make(chan int,3)
()
chan 的零值是 nil,close 一个 nil 通道会引发 panic。往 nil 通道写入或从中读取数据会永久阻塞.
2. 读写
-
写:
ch <- x,将x发送到 chan 中 -
读:
x = <-ch,从chan中接收,保存到x中 -
读(忽略返回值):
<-ch,从ch中接收,忽略接收到的结果 -
读(判断是否是关闭前发送的):
x, ok := <-ch,使用了两个返回值接收,第二个返回值表明了接收到的x是不是chan关闭之前发送进去的,true就代表是。
3. 缓冲
- 无缓冲的
chan会在发送的时候阻塞,直到有另一个协程从chan中获取数据(make时候不传递或者传 0 表示 chan 本身不能存储数据是无缓冲的,go 底层会直接在两个 goroutine 之间传递,而不经过 chan 的复制。). - 有缓冲的
chan在协程往里面写入数据的时候,可以进行缓冲。在需要读取 chan 的 goroutine 的处理速度比较慢的时候,写入 chan 的 goroutine 也可以持续运行,直到写满 chan 的缓冲区,如果chan的数据一直没有被接收,然后满了的时候,往chan写入数据的协程依然会陷入阻塞。但这种阻塞状态会在chan的数据被读取的时候解除(如果make时候第二个参数大于 0,我们往 chan 写数据的时候,会先复制到 chan 这个数据结构,然后其他的 goroutine 从 chan 中读取数据的时候,chan 会将数据复制到这个 goroutine 中)。
4. len和cap
len:通过len我们可以查询一个chan的长度,也就是有多少被发送到这个chan但是还没有被接收的值。cap:通过cap可以查询一个容道的容量,也就是我们传给make函数的第二个参数,它表示chan最多可以容纳多少数据。- 如果
chan是nil,那么len和cap都会返回 0。
5. chan 的方向
chan,没有指定方向,既可以读又可以写。chan<-,只写chan,只能往chan中写入数据<-chan,只读chan,只能从chan中读取数据- 无方向的
chan可以转换为chan<-或者<-chan,但是反过来不行
package main
import "fmt"
var done = make(chan struct{})
// ch 是只写 chan,如果在这个函数里面从 ch 读取数据编译不会通过
func producer(ch chan<- int) {
for i := 0; i < 3; i++ {
ch <- i
fmt.Printf("produce %d\n", i)
}
// 发送 3 个数之后,关闭 chan
close(ch)
}
// ch 是只读 chan,如果在这个函数里往 ch 写入数据编译不会通过
func consumer(ch <-chan int) {
for {
i, ok := <-ch
if !ok {
// chan 的数据已经被全部接收完,
// 发送 done 信号
done <- struct{}{}
break
}
fmt.Printf("consume %d\n", i)
}
}
func main() {
nums := make(chan int, 10)
go producer(nums)
go consumer(nums)
// 收到结束信号之后继续往下执行
<-done
}
6. for...range 语句
从 chan 读取数据的时候,可能需要用两个值来接收 chan 的返回值,第二个值用来判断接收到的值是否是 chan 关闭之前发送的。
而 for...range 语法也可以用来从 chan 中读取数据,它会循环,直到 chan 关闭,直接免去了判断的操作
package main
import "fmt"
func main() {
done := make(chan struct{})
nums := make(chan int)
go func() {
for i := 0; i < 3; i++ {
fmt.Printf("send %d\n", i)
nums <- i
}
close(nums)
}()
go func() {
// 传统写法
//for {
// num, ok := <-nums
// if !ok {
// break
// }
// fmt.Printf("receive %d\n", num)
//}
// range 语法糖
for num := range nums {
fmt.Printf("receive %d\n", num)
}
done <- struct{}{}
}()
<-done
}
7. select语句
go 里面有一个关键字 select,可以让我们同时监听几个 chan,在任意一个 chan 有数据的时候,select 里面的 case 块得以执行:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// ch1 会先收到数据
go func() {
time.Sleep(time.Second)
ch1 <- 1
}()
go func() {
time.Sleep(time.Second * 2)
ch2 <- 1
}()
// select 会阻塞,直到其中某一个分支收到数据
select {
case <-ch1:
// 执行这一行代码
fmt.Println("from ch1")
case <-ch2:
// 这一行不会被执行
fmt.Println("from ch2")
}
}
select-case 的用法类似于 switch-case,也有一个 default 语句,在 select 里面
- 如果
default之前的case都不满足,则执行default块的代码。 - 如果没有
default语句,则会一直阻塞,直到某一个case上面的chan返回(有数据、或者chan被关闭都会返回)
当然,case 后面可以从 chan 读取数据,也可以往 chan 写数据,比如:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
// 往 nil chan 写入数据会阻塞
var ch2 chan int
// ch1 会先收到数据
go func() {
time.Sleep(time.Second)
ch1 <- 1
}()
// 会阻塞,直到其中一个 case 返回
select {
case <-ch1:
// 执行这一行代码
fmt.Println("from ch1")
case ch2 <- 1: // 永远不会满足,因为 ch2 是 nil
fmt.Println("from ch2")
}
}
8. select 常见用法
select 的一种很常见的用法是,等待一个 chan 和一个定时器(实现控制超时的功能),比如:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
// ch1 一秒后才收到数据
go func() {
time.Sleep(time.Second)
ch1 <- 1
}()
select {
case <-ch1:
fmt.Println("from ch1")
case <-time.After(time.Millisecond * 100):
// 执行如下代码,因为这个 case 在 100ms 后就返回了
fmt.Println("from ch2")
}
}
如果需要控制某些操作的超时时间,那么就可以在时间到了之后,做一些清理操作,然后终止一些工作,最后退出协程。