Golang—channel
Golang协程-CSDN博客,了解了Golang中的协程再来讲协程之间如何通过channel进行通信。
了解channel
概念:传送带 / 管道
你可以把 Channel(通道) 想象成一条在协程(Goroutine) 之间传送数据的传送带或者管道。
-
协程(Goroutine):就像工厂里的工人。
-
Channel(通道):就像连接两个工人工作台的传送带。
Channel 的主要作用
-
通信(Communication)
-
这是最基本的作用。一个协程把数据(比如一个零件)放在传送带的一端(发送),另一个协程从传送带的另一端取走这个数据(接收)。数据就成功地从 A 协程传递到了 B 协程。
-
-
同步(Synchronization)
-
这是 Channel 一个极其重要的“副作用”。它保证了协程之间的执行顺序。
-
没有缓冲区的 Channel(Unbuffered Channel) 就像一次只能传一个零件的、手递手的传送带。
-
发送者把零件放上传送带后,必须等待接收者把它取走,才能继续干下一件事(发送下一个数据)。
-
接收者在零件到达之前,必须等待发送者把零件放上来。
-
这个过程强制了两个协程在同一个时间点“碰头”,完成了同步。
-
-
-
防止竞态条件(Preventing Race Conditions)
-
多个工人(协程)如果同时去操作一个共享的工具箱(共享变量),可能会发生争抢,导致数据错乱。这被称为“竞态条件”。
-
通过 Channel,我们可以把“操作共享数据”这个任务,变成一个“通过传送带传递任务”的模型。比如,我们指定只有一个特殊的“管理员”协程可以操作工具箱,其他协程如果需要工具,就把请求(数据)通过 Channel 发给管理员,然后等待管理员通过另一个 Channel 把工具(结果)送回来。这样就避免了多个协程直接冲突。
-
生动的场景举例
假设我们有一个主线程(厂长)和两个协程(工人A和工人B)。
没有 Channel 的世界(混乱):
-
厂长说:“A去生产零件,B去组装。”
-
A和B同时跑向仓库拿原料,可能会撞在一起(竞态条件)。
-
B可能跑得太快,在A还没生产出零件时,就开始组装空气(数据不同步)。
有 Channel 的世界(井然有序):
场景一:简单的通信与同步(使用无缓冲Channel)
// 创建一个传送带(无缓冲Channel)
ch := make(chan string)// 工人A(协程)
go func() {result := "零件A做好了" // 1. 工人A生产零件ch <- result // 2. 把零件放上传送带。此时如果没人来接,A就等着(阻塞)fmt.Println("A:我把零件交出去了")
}()// 主线程(厂长)
message := <-ch // 3. 厂长从传送带取下零件。如果零件没到,厂长就等着(阻塞)
fmt.Println("主线程收到了:", message)
// 输出:
// 主线程收到了: 零件A做好了
// A:我把零件交出去了
看,因为 Channel 的同步特性,A:我把零件交出去了
这句话总是在零件被主线程接收之后才打印。
场景二:带缓冲的通信(像一个小型仓库)
// 创建一个能存放2个零件的传送带(缓冲为2的Channel)
ch := make(chan string, 2)// 工人A可以连续放两个零件,而不用马上等别人来取
ch <- "零件1"
ch <- "零件2"
// ch <- "零件3" // 如果此时再放第三个,因为仓库满了,工人A就会阻塞等待// 工人B可以连续取走两个零件
fmt.Println(<-ch) // 零件1
fmt.Println(<-ch) // 零件2
这种 Channel 更侧重于通信的吞吐量,而不是强同步。
总结
所以,通俗地讲,Channel 的作用就是:
它为协程们提供了一条安全、有序的“数据传输管道”。不仅解决了“怎么传”的通信问题,更重要的是通过“等待”机制,巧妙地解决了“什么时候传”的同步问题,从而让并发编程变得简单和安全。
channel的基本定义与使用
定义
make(chan Type) //无缓冲 等价于make(chan Type,0)
make(chan Type, capacity) //有缓冲
channel <- value //发送value到channel
<- channel //接收并将其丢弃
x := <- channel //从channel中接收数据,并赋值给x
x, ok := <- channel //功能同上,同时检查通道是否已关闭或者是否为空
使用
import ("fmt"
)func main() {//定义一个channelc := make(chan int)//启动一个goroutine,向channel中发送数据go func() {defer fmt.Println("goroutine end")fmt.Println("goroutine start to send data")c <- 666 //向channel中发送数据666}()num := <-c //从channel中接收数据并赋值给numfmt.Println("main received data:", num)fmt.Println("main goroutine end")
}---------------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
goroutine start to send data
goroutine end
main received data: 666
main goroutine end
channel的有缓冲和无缓冲
无缓冲
- 第1步,两个 goroutine 都到达通道,但哪个都没有开始执行发送或者接收。
- 第 2步,左侧的 goroutine 将它的手伸进了通道,这模拟了向通道发送数据的行为。这时,这个 goroutine 会在通道中被锁住,直到交换完成。
- 第 3 步,右侧的 goroutine 将它的手放入通道,这模拟了从通道里接收数据。这个 goroutine 一样也会在通道中被锁住,直到交换完成。
- 第 4步和第 5 步,进行交换,并最终,在第6步,两个goroutine 都将它们的手从通道里拿出来,这模拟了被锁住的 goroutine 得到释放。两个 goroutine 现在都可以去做其他事情了。
有缓冲
- 第1步,右侧的 goroutine 正在从通道接收一个值。
- 第 2步,右侧的这个 goroutine独立完成了接收值的动作,而左侧的goroutine 正在发送一个新值到通道里。
- 第 3 步,左侧的goroutine 还在向通道发送新值,而右侧的 goroutine 正在从通道接收另外一个值。这个步骤里的两个操作既不是同步的,也不会互相阻塞。
- 最后,第4步,所有的发送和接收都完成,而通道里还有几个值,也有一些空间可以存更多的值。
- 发生阻塞的情况:如果左侧放满了右侧还没取左侧会阻塞、或者右侧取空了左侧还没放右侧会阻塞
在上述定义中的实例是无缓冲channel,下面做有缓冲channel的示范
package mainimport ("fmt""time"
)func main() {//定义一个有缓冲channelc := make(chan int, 3)fmt.Println("len(c) = ", len(c), ",cap(c) = ", cap(c))//启动一个goroutine,向channel中发送数据go func() {defer fmt.Println("goroutine end")for i := 0; i < 3; i++ {c <- i //向channel中发送数据fmt.Println("goroutine start to send data:", i, "len(c)=", len(c), ",cap(c)=", cap(c))}}()time.Sleep(2 * time.Second)for i := 0; i < 3; i++ {num := <-c //从channel中接收数据fmt.Println("main goroutine receive data:", num, "len(c)=", len(c), ",cap(c)=", cap(c))}fmt.Println("main goroutine end")
}---------------------------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
len(c) = 0 ,cap(c) = 3
goroutine start to send data: 0 len(c)= 1 ,cap(c)= 3
goroutine start to send data: 1 len(c)= 2 ,cap(c)= 3
goroutine start to send data: 2 len(c)= 3 ,cap(c)= 3
goroutine end
main goroutine receive data: 0 len(c)= 2 ,cap(c)= 3
main goroutine receive data: 1 len(c)= 1 ,cap(c)= 3
main goroutine receive data: 2 len(c)= 0 ,cap(c)= 3
main goroutine end
上述可以看到协程发送完数据结束,主线程接收完数据结束。
如果发生或接收数据超过channel容量:
func main() {//定义一个有缓冲channelc := make(chan int, 3)fmt.Println("len(c) = ", len(c), ",cap(c) = ", cap(c))//启动一个goroutine,向channel中发送数据go func() {defer fmt.Println("goroutine end")for i := 0; i < 4; i++ {c <- i //向channel中发送数据fmt.Println("goroutine start to send data:", i, "len(c)=", len(c), ",cap(c)=", cap(c))}}()time.Sleep(2 * time.Second)for i := 0; i < 4; i++ {num := <-c //从channel中接收数据fmt.Println("main goroutine receive data:", num, "len(c)=", len(c), ",cap(c)=", cap(c))}fmt.Println("main goroutine end")
}--------------------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
len(c) = 0 ,cap(c) = 3
goroutine start to send data: 0 len(c)= 1 ,cap(c)= 3
goroutine start to send data: 1 len(c)= 2 ,cap(c)= 3
goroutine start to send data: 2 len(c)= 3 ,cap(c)= 3
main goroutine receive data: 0 len(c)= 3 ,cap(c)= 3
main goroutine receive data: 1 len(c)= 2 ,cap(c)= 3
main goroutine receive data: 2 len(c)= 1 ,cap(c)= 3
main goroutine receive data: 3 len(c)= 0 ,cap(c)= 3
main goroutine end
超过容量协程继续发送数据就会阻塞协程不会提前结束,而主线程已经结束了。(输出也会有不同的情况,不过相同的情况是协程会发生阻塞不会提前结束)
关闭channel
通过close()来关闭channel
正常关闭
package mainimport ("fmt"
)func main() {//定义一个channelc := make(chan int)//启动一个goroutine,向channel中发送数据go func() {for i := 0; i < 5; i++ {c <- i}//close可以关闭channelclose(c)}()for {//ok如果为true,表示channel没有关闭,如果为false表示channel已经关闭if data, ok := <-c; ok {fmt.Println(data)} else {break}}fmt.Println("main goroutine end")
}---------------------------------------------------------------------------PS D:\GoProject\firstGoProject> go run firstGoProject.go
0
1
2
3
4
main goroutine end
没有关闭
没有关闭会报死锁的错误,因为协程中已经向channel发送完毕数据了,主线程中依然还在等待数据导致主函数阻塞。(对应缓冲中提到过的阻塞情况)
func main() {//定义一个channelc := make(chan int)//启动一个goroutine,向channel中发送数据go func() {for i := 0; i < 5; i++ {c <- i}//close可以关闭channel//close(c)}()for {//ok如果为true,表示channel没有关闭,如果为false表示channel已经关闭if data, ok := <-c; ok {fmt.Println(data)} else {break}}fmt.Println("main goroutine end")
}---------------------------------------------------------------------------PS D:\GoProject\firstGoProject> go run firstGoProject.go
0
1
2
3
4
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive]:
main.main()D:/GoProject/firstGoProject/firstGoProject.go:23 +0xbd
exit status 2
注:
- channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
- 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
- 关闭channel后,可以继续从channel接收数据;
- 对于nil channel,无论收发都会被阻塞。
channel与range
func main() {//定义一个channelc := make(chan int)//启动一个goroutine,向channel中发送数据go func() {for i := 0; i < 5; i++ {c <- i}//close可以关闭channelclose(c)}()// for {// //ok如果为true,表示channel没有关闭,如果为false表示channel已经关闭// if data, ok := <-c; ok {// fmt.Println(data)// } else {// break// }// }//使用for range遍历channel,效果与注释掉的代码相同for data := range c {fmt.Println(data)}fmt.Println("main goroutine end")
}----------------------------------------------------------------------------PS D:\GoProject\firstGoProject> go run firstGoProject.go
0
1
2
3
4
main goroutine end
channel与select
单流程下一个go只能监控一个channel的状态,select可以完成监控多个channel的状态。
package mainimport ("fmt"
)func fibonacci(c, quit chan int) {x, y := 1, 1for {select {case c <- x://如果c可写,则case就会进来x = yy = x + ycase <-quit://如果quit可读,则case就会进来fmt.Println("quit")return}}
}func main() {c := make(chan int)quit := make(chan int)//sub gogo func() {for i := 0; i < 6; i++ {fmt.Println(<-c)}quit <- 0 //通知子go退出}()//main gofibonacci(c, quit)
}---------------------------------------------------------------------PS D:\GoProject\firstGoProject> go run firstGoProject.go
1
1
2
4
8
16
quit
select {case <- chanl://如果chan1成功读到数据,则进行该case处理语句case chan2 <- 1://如果成功向chan2写入数据,则进行该case处理语句default://如果上面都没有成功,则进入defau1t处理流程
}