Go(四)Channel

在 Go 语言中,Channel(通道) 是并发编程的核心组件之一。如果说 Goroutine 是 Go 并发执行的实体,那么 Channel 就是这些实体之间沟通的桥梁。

Go 语言有一句非常经典的并发哲学:

"Do not communicate by sharing memory; instead, share memory by communicating." > (不要通过共享内存来通信,而应该通过通信来共享内存。)

1. Channel 的基本操作

2. 无缓冲 vs 有缓冲

这是理解 Channel 行为最关键的地方,它们的阻塞机制完全不同。

无缓冲 Channel (同步通信)

默认通过 make(chan T) 创建的都是无缓冲 Channel。

  • 特性: 它没有内部容量。发送方和接收方必须同时准备好,数据才能传递成功。

  • 阻塞场景: * 如果发送方先执行 ch <- data,它会被阻塞 ,直到有另一个 Goroutine 执行 <-ch

    • 反之,如果接收方先执行 <-ch,它也会被阻塞,直到有 Goroutine 向其发送数据。
  • 比喻: 就像接力赛跑,交棒人和接棒人必须同时在交接区,棒子才能递过去。

有缓冲 Channel (异步通信)

通过 make(chan T, capacity) 创建,带有初始容量。

  • 特性: 内部有一个队列(Buffer)。只要队列没满,发送方就可以一直发送;只要队列没空,接收方就可以一直接收。

  • 阻塞场景:

    • 发送方阻塞: 只有当缓冲区已满时,继续发送才会阻塞。

    • 接收方阻塞: 只有当缓冲区为空时,继续接收才会阻塞。

  • 比喻: 就像快递柜。快递员(发送方)只要看到有空柜子就可以把件放进去(不阻塞),不需要等客户。只有快递柜满了,快递员才需要等。客户(接收方)只要柜子里有自己的件就能取,只有柜子空了才需要等。

简单来说,Channel 的底层是一个带锁的环形队列(Ring Buffer) ,外加两个用于存放阻塞 Goroutine 的双向链表 ,并且深度集成了 Go 的 GMP 调度器

2. 发送数据的底层逻辑 (ch <- data)

当一个 Goroutine 执行发送操作时,底层会按照以下顺序进行判断和处理:

  1. 直接交接(Direct Handoff - 性能最高):

    • 检查 recvq(接收等待队列)是否为空。

    • 如果不为空 ,说明有接收者正在苦苦等待。此时,Go 会做一件非常巧妙的事:直接将数据从发送方 Goroutine 的栈内存,拷贝到接收方 Goroutine 的栈内存中,然后唤醒那个睡眠的接收方 Goroutine。

    • 优势: 完全绕过了 buf,减少了一次内存拷贝,效率极高。

  2. 写入缓冲区(Write to Buffer):

    • 如果 recvq 为空,并且 Channel 有缓冲区且未满(qcount < dataqsiz)。

    • 发送方会把数据拷贝到环形队列 bufsendx 位置,然后将 sendx 向前移动一格,qcount 加一。整个过程结束。

  3. 阻塞睡眠(Block):

    • 如果缓冲区也满了,或者根本就是无缓冲 Channel。

    • 发送方 Goroutine 会被打包成一个 sudog 节点,挂载到 sendq(发送等待队列)中。

    • 然后调用 gopark 让出 CPU 的执行权,当前 Goroutine 进入休眠,等待被别人唤醒。

3. 接收数据的底层逻辑 (<-ch)

接收逻辑与发送逻辑基本是对称的:

  1. 直接交接或满缓冲拉取:

    • 检查 sendq 是否有等待的发送者。

    • 如果有,且是无缓冲 Channel:直接从发送者的栈上把数据拷贝过来,然后唤醒发送者。

    • 如果有,且是有缓冲 Channel(说明缓冲区满了):先从环形队列的头部(recvx)取走一个数据,然后把 sendq 中那个等待的发送者的数据放入队列的尾部(sendx),最后唤醒发送者。

  2. 从缓冲区读取:

    • 如果没有人在等待发送,且缓冲区里有数据(qcount > 0)。

    • bufrecvx 位置读取数据,recvx 向前移动一格,qcount 减一。

  3. 阻塞睡眠:

    • 如果缓冲区为空,或者无缓冲且没有发送者。

    • 接收方 Goroutine 被打包成 sudog,放入 recvq 队列,调用 gopark 休眠,等待发送者带着数据来唤醒它。

Go 复制代码
package main
import (
 "fmt"
 "time"
)
func main() {
c := make(chan int, 3) //带有缓冲的channel 缓冲空间为3
 fmt.Println("c is ", c, " len is ", len(c), "cap is", cap(c))
 go func() {
  defer fmt.Println("goroutine end")
  for i := 0; i < 10; i++ {
   c <- i
   fmt.Println("子go在运行,值为", i, "len is", len(c), "cap is", cap(c))
  }
 }()
 time.Sleep(2 * time.Second)
 for i := 0; i < 10; i++ {
  num := <-c
  fmt.Println("main中的管道值接收到了,值为", num)
 }
 fmt.Println("main end")
}
GOROOT=D:\Code_software\Go_software #gosetup
GOPATH= #gosetup
D:\Code_software\Go_software\bin\go.exe build -o C:\Users\weixu\AppData\Local\JetBrains
\GoLand2025.3\tmp\GoLand\___go_build_test_channel1_go.exe D:\Code_software\Go_software\
Project_go_25_12_30\test-goroutine\test-channel1.go #gosetup
C:\Users\weixu\AppData\Local\JetBrains\GoLand2025.3\tmp\GoLand\___go_build_test_
channel1_go.exe #gosetup
c is  0xc0000a2000  len is  0 cap is 3
子go在运行,值为 0 len is 1 cap is 3
子go在运行,值为 1 len is 2 cap is 3
子go在运行,值为 2 len is 3 cap is 3
main中的管道值接收到了,值为 0
main中的管道值接收到了,值为 1
main中的管道值接收到了,值为 2
main中的管道值接收到了,值为 3
子go在运行,值为 3 len is 0 cap is 3
子go在运行,值为 4 len is 0 cap is 3
子go在运行,值为 5 len is 1 cap is 3
子go在运行,值为 6 len is 2 cap is 3
子go在运行,值为 7 len is 3 cap is 3
main中的管道值接收到了,值为 4
main中的管道值接收到了,值为 5
main中的管道值接收到了,值为 6
main中的管道值接收到了,值为 7
main中的管道值接收到了,值为 8
子go在运行,值为 8 len is 0 cap is 3
子go在运行,值为 9 len is 0 cap is 3
goroutine end
main中的管道值接收到了,值为 9
main end
Process finished with the exit code 0

3、channel与range

4、select的使用

5、go之间通信

通过函数参数显式传递 (最推荐)

Go 复制代码
package main

import "fmt"

// worker 函数接收一个专门处理 int 的 channel
func worker(id int, ch chan int) {
    // 因为 main 函数把 ch 的指针传进来了,所以它明确知道往哪个 channel 发数据
    ch <- id * 10
}

func main() {
    // 1. 创建一根实物水管 (底层是在堆上分配了 hchan,ch 拿到了内存地址)
    myChannel := make(chan int)

    // 2. 启动多个 Goroutine,并把这根水管的地址"递"给它们
    go worker(1, myChannel)
    go worker(2, myChannel)

    // 3. 从这根水管里取两次数据
    fmt.Println(<-myChannel)
    fmt.Println(<-myChannel)
}
相关推荐
未若君雅裁1 小时前
Java 线程基础:进程、线程、并发并行、创建方式与生命周期
java·开发语言
橘子星1 小时前
前端薅数据神器 Fetch:不用翻墙,在线拿捏后端与 AI 接口
前端·后端
sugar__salt1 小时前
JS正则表达式与字符串高阶实战精讲
开发语言·javascript·正则表达式
AI浩1 小时前
梯度累积与 Micro-Batch 设计分层式精讲:有效批次、显存边界与分布式同步
开发语言·分布式·batch
未若君雅裁2 小时前
死锁产生条件与诊断:jps、jstack、VisualVM
java·开发语言
再玩一会儿看代码2 小时前
Java抽象类和接口区别_场景理解
java·开发语言·经验分享·笔记·python
用户925807911482 小时前
画图理解mysql日志机制
java·后端
huzhongqiang2 小时前
120行代码实现一个极简 Agent
后端·agent
XIAOHEZIcode2 小时前
进程、会话与终端——一次真实的 Linux Session 解剖
linux·后端·命令行