channel 在 Go 中是一种专门用来在 goroutine 之间传递数据的类型安全的管道。
你可以把它理解成:
Go 语言采用 CSP(Communicating Sequential Processes) 模型,也就是鼓励:
“不要通过共享内存来通信,而要通过通信来共享内存”
也就是通过 channel 来传递数据,而不是多个 goroutine 同时操作一份共享数据,这样能减少复杂的锁。
✅ 类型化:
通道是有类型的,比如 chan int 只能传 int,chan string 只能传 string。
✅ 同步:
默认情况下,不缓冲通道是同步阻塞:
✅ 缓冲区可选:
你也可以给通道设置缓冲区,比如 make(chan int, 10),这样最多能存 10 个 int,不满时发送方不会阻塞。
✅ 关闭通道:
当通道关闭(close(ch))后再接收,不会再阻塞,而是返回对应类型的零值和一个标志。
go func() {
channel <- "Hello, World!"
}()
这里启动了一个新的 goroutine 来往通道中发送数据。
go func() { … }() 相当于让任务异步执行,也就是你说的 Python 里的 create_task() 或者 await 背后的任务调度
当无缓冲通道必须一手给一手拿,所以你必须用 goroutine 来让另一个任务并行接收,否则就死锁。
假设你去掉 go func(),直接写成:
channel := make(chan string)
channel <- "Hello, World!" // 直接发送
msg := <-channel
fmt.Println(msg)
那么这一行 channel <- “Hello, World!” 会一直卡住,因为:
用 go func() 把发送操作放到后台 goroutine 中,这样主 goroutine 可以继续执行接收逻辑,不再相互卡住。
换句话说:
如果没有 goroutine,把发送和接收写在一条顺序执行流里,就造成死锁。
❓ 所有 channel 都必须这样写吗?
不必须,取决于场景:
ch := make(chan string, 1)
ch <- "Hello"
msg := <-ch
• 如果你的接收方先执行,比如:
ch := make(chan string)
go func() {
msg := <- ch
fmt.Println(msg)
}()
ch <- "Hello"
无缓冲通道要求
发送和接收必须成对并发执行
也可以用缓冲通道,这样中间能暂存数据,就不用 goroutine。
类别 | 定义 | 行为 |
---|---|---|
无缓冲通道 | ch := make(chan string) | 必须一手交钱一手交货(也就是发送时必须有接收方正在等着),否则发送方就堵住,不往下执行 |
有缓冲通道 | ch := make(chan string, N) | 通道内部有个缓冲区,当缓冲区没满时可以先把数据存起来,不需要接收方马上接收,发送方能继续往下走 |
为什么无缓冲通道单线程会卡?
无缓冲通道相当于是零容量队列,你要往里面塞东西,但是根本没有位置存,所以必须有人正在取,这时候才成对地完成操作。
你原来的例子:
channel := make(chan string) // 无缓冲通道
channel <- "Hello" // <=== 这里堵住了
msg := <-channel
为什么堵住?
因为这一行执行时:
用 Python 类比一下也许更清楚:
没缓冲通道:必须有接收方同时存在,否则卡死,所以你要用 goroutine 来接收。
有缓冲通道:通道能先存东西,不必马上接收,所以可以顺利执行完发送,不用 goroutine。
对比点 | 普通 int 变量 | chan int 通道 |
---|---|---|
类型 | int 只存一个数字 | chan int 用来传送数字给别人 |
用法 | num := 42 然后用 num | ch := make(chan int) 然后用 ch <- 42 传数据 |
并发场景 | 只在当前 goroutine用 | 能在多个 goroutine之间传递数据,不共享状态 |
行为 | 单纯取值、赋值 | 发送必须有接收方,不然 goroutine 会等着 |
目的 | 存数据 | 让 goroutine 之间通信 |
相当于什么现实场景?
想象你有两个工人:
那么:
你可以用共享变量,但是:
用 channel 你就不用自己实现这些复杂逻辑,Go 会帮你:
✅ 发送数据时自动等待接收方。
✅ 接收数据时自动等待生产方。
✅ 并发安全,不用你加锁。