说明
channel是go当中的一个核心类型,可以看做是管道。并发核心单元可以通过channel进行数据的发送和接收,从而实现通信。
在go中,channel是一种数据类型,主要被用来解决协程的同步问题以及协程之间数据共享(数据传递)的问题。
go当中的goroutine运行在相同的地址空间,因此访问共享内存地址必须做好同步,goroutine奉行通过通信来共享内存,而不是共享内存来通信。
引用类型channel可用于多个goroutine通讯,在其内部实现了同步,确保并发安全。
定义channel变量
在go中,channel也一个对应make创建的底层数据结构的引用。
当我们复制一个channel或者用于函数参数传递的时候,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象,和其他的引用类型一样,channel的零值也是nil。
定义一个channel的时候,也需要定义发送到channel的值的类型,channel可以使用内置的make函数来进行创建。
chan是创建channel使用的关键字,Type代表着channel收发数据的类型。
make(chan Type) // 等价于make(chan Type,0)
make(chan Type,capacity)
当 参数capacity= 0 时,channel 是无缓冲阻塞读写的;当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。
channel非常像生活中的管道,一边可以存放东西,另一边可以取出东西。channel通过操作符 <- 来接收和发送数据,发送和接收数据语法:
package main
import "fmt"
func main() {
// 创建一个channel
ch := make(chan int)
// 创建一个子goroutine,在这个goroutine中进行数据的写入
go worker(ch)
// 读取channel通道中的数据
num := <- ch
fmt.Println("channel通道中读到的数据为:",num)
}
func worker(ch chan int) {
// 向channel通道中写入数据
ch <- 10
}
在上面的代码中,我们通过chan <- val 来进行channel值的写入,通过val := <- chan 来进行channel的值的读取。
我们首先创建了一个goroutine用来向channel管道中进行数据的写入,然后同时在main goroutine中进行数据的读取。
输出的结果为:
channel通道中读到的数据为: 10
Process finished with exit code 0
如果channel(无缓冲)的一头发生了读操作或者写操作,而在另外一头没有进行相对应的操作,那么就会发生阻塞,直到channel的另一头发生相应的操作。
默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。
package main
import (
"fmt"
)
func main() {
c := make(chan int)
go func() {
defer fmt.Println("子协程结束")
fmt.Println("子协程正在运行……")
c <- 666 //666发送到c
}()
num := <-c //从c中接收数据,并赋值给num
fmt.Println("num = ", num)
fmt.Println("main协程结束")
}
程序运行结果:
当进行读取数据的时候,可以采用下面的形式来判断数据是否传输完毕:
x, ok := <-channel
如果数据传输完毕,ok的值为false,否则为true。
package main
import "fmt"
func main() {
// 创建一个channel
ch := make(chan int)
// 创建一个goroutine用来向channel中写入数据
go func() {
defer close(ch) // 通过close函数可以关闭channel通道
for i:=0;i<5;i++ {
ch <- i // 将i值写入channel
}
}()
// 在主goroutine中读取channel通道传递的值
for {
val,ok := <-ch
if !ok { // ok 为false时表示读取结束
fmt.Println("读取结束,over")
break
}
fmt.Println("读取到的数据为:",val)
}
}
输出的结果为:
读取到的数据为: 0
读取到的数据为: 1
读取到的数据为: 2
读取到的数据为: 3
读取到的数据为: 4
读取结束,over
Debugger finished with exit code 0
定义无缓冲的channel
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。
这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。否则,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。
这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
例如上面我们写的程序其实都是无缓冲的channel。
阻塞:由于某种原因数据没有到达,当前协程(线程)持续处于等待状态,直到条件满足,才接触阻塞。
同步:在两个或多个协程(线程)间,保持数据内容一致性的机制。
下图展示两个 goroutine 如何利用无缓冲的通道来共享一个值:
定义有缓冲的channel
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个数据值的通道。
这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也不同。
只有通道中没有要接收的值时,接收动作才会阻塞。
只有通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
示例图如下:
有缓冲的channel创建格式:
make(chan Type, capacity)
如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。
package main
import "fmt"
func main() {
// 定义一个有缓冲的channel
ch := make(chan int,2)
// 创建一个goroutine,用来写入数据
go func() {
defer close(ch)
for i:=0;i<5;i++ {
ch <- i
fmt.Println("写入的数据为:",i) // print操作属于io操作,可能导致cpu时间片切换
}
}()
for {
num,ok := <- ch
if !ok {
fmt.Println("读取结束,over")
break
}
fmt.Println("读取到的数据为:",num)
}
}
运行的结果为:
写入的数据为: 0
读取到的数据为: 0
读取到的数据为: 1
写入的数据为: 1
写入的数据为: 2
写入的数据为: 3
写入的数据为: 4
读取到的数据为: 2
读取到的数据为: 3
读取到的数据为: 4
读取结束,over
Process finished with exit code 0
关闭channel
如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现。
package main
import (
"fmt"
)
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//把 close(c) 注释掉,程序会一直阻塞在 if data, ok := <-c; ok 那一行
close(c)
}()
for {
//ok为true说明channel没有关闭,为false说明管道已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Finished")
}
程序运行结果:
注意:
-
channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
-
关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
-
关闭channel后,可以继续从channel接收数据;
-
对于nil channel,无论收发都会被阻塞。
可以使用 range 来迭代不断操作channel:
package main
import (
"fmt"
)
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//把 close(c) 注释掉,程序会一直阻塞在 for data := range c 那一行
close(c)
}()
for data := range c {
fmt.Println(data)
}
fmt.Println("Finished")
}
单向channel
默认情况下,通道channel是双向的,也就是,既可以往里面发送数据也可以同里面接收数据。
但是,我们经常见一个通道作为参数进行传递而值希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。
单向channel变量的声明非常简单,如下:
var ch1 chan int // ch1是一个正常的channel,是双向的
var ch2 chan<- float64 // ch2是单向channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读int数据
// chan<- //只写
func counter(out chan<- int) {
defer close(out)
for i := 0; i < 5; i++ {
out <- i //如果对方不读 会阻塞
}
}
// <-chan //只读
func printer(in <-chan int) {
for num := range in {
fmt.Println(num)
}
}
func main() {
c := make(chan int) // chan //读写
go counter(c) //生产者
printer(c) //消费者
fmt.Println("done")
}