Go Channel
Channel 是 Go 并发编程中 goroutine 之间通信的核心机制。Go 的哲学是"不要通过共享内存来通信,而应通过通信来共享内存"。Channel 就像一个类型安全的管道,让 goroutine 之间可以安全地传递数据,无需手动加锁。
创建 channel
使用 make 函数创建 channel,chan 关键字声明 channel 类型。
package main
import "fmt"
func main() {
// 创建无缓冲 channel
ch1 := make(chan int)
// 创建有缓冲 channel(容量为 10)
ch2 := make(chan string, 10)
fmt.Printf("ch1 类型: %T, 容量: %d\n", ch1, cap(ch1))
fmt.Printf("ch2 类型: %T, 容量: %d\n", ch2, cap(ch2))
}make(chan T) 创建无缓冲 channel,make(chan T, capacity) 创建有缓冲 channel。容量为 0 或省略时为无缓冲。
发送和接收
<- 操作符用于发送和接收数据。方向取决于它在 chan 的哪一侧。
package main
import (
"fmt"
"time"
)
func sender(ch chan int) {
for i := 1; i <= 5; i++ {
ch <- i // 发送到 channel
fmt.Printf("发送: %d\n", i)
time.Sleep(50 * time.Millisecond)
}
close(ch) // 发送完毕后关闭
}
func receiver(ch chan int, done chan bool) {
for {
val, ok := <-ch // 从 channel 接收
if !ok {
fmt.Println("channel 已关闭")
done <- true
return
}
fmt.Printf("接收: %d\n", val)
}
}
func main() {
ch := make(chan int)
done := make(chan bool)
go sender(ch)
go receiver(ch, done)
<-done // 等待 receiver 完成
fmt.Println("程序结束")
}ch <- value 发送数据,value := <-ch 接收数据。从已关闭的 channel 接收会返回零值和 ok = false。
无缓冲 channel(同步通信)
无缓冲 channel 的发送和接收必须同时就绪,否则先执行的一方会阻塞。这实现了 goroutine 之间的同步。
package main
import (
"fmt"
"time"
)
func ping(ch chan string) {
msg := <-ch // 接收
fmt.Println("ping 收到:", msg)
ch <- "pong" // 回复
fmt.Println("ping 已回复")
}
func main() {
ch := make(chan string) // 无缓冲 channel
go ping(ch)
// 主 goroutine 先发送
ch <- "hello" // 如果 ping 还没准备好接收,这里会阻塞
fmt.Println("main 已发送")
// 等待 ping 的回复
reply := <-ch
fmt.Println("main 收到:", reply)
}无缓冲 channel 保证了发送方和接收方的"握手"——双方都到达时数据才会传递。这种特性常用于 goroutine 之间的信号同步。
有缓冲 channel(异步通信)
有缓冲 channel 允许在无人接收时暂存数据,发送方只在缓冲区满时才阻塞。
package main
import "fmt"
func producer(ch chan int, count int) {
for i := 0; i < count; i++ {
ch <- i
fmt.Printf("生产: %d (缓冲: %d/%d)\n", i, len(ch), cap(ch))
}
close(ch)
}
func main() {
ch := make(chan int, 5) // 缓冲区容量为 5
go producer(ch, 8)
// 等待生产者完成一部分后开始消费
for val := range ch {
fmt.Printf("消费: %d (缓冲: %d/%d)\n", val, len(ch), cap(ch))
}
fmt.Println("所有数据处理完毕")
}有缓冲 channel 的 len(ch) 返回当前缓冲的元素个数,cap(ch) 返回缓冲区容量。当缓冲区满时发送阻塞,当缓冲区空时接收阻塞。
channel 方向
可以限制 channel 只能发送或只能接收,增强代码的类型安全性。
package main
import "fmt"
// producer 只发送数据
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
// consumer 只接收数据
func consumer(ch <-chan int) {
for val := range ch {
fmt.Printf("收到: %d\n", val)
}
}
func main() {
ch := make(chan int, 5)
go producer(ch) // chan int 可以隐转换为 chan<- int
consumer(ch) // chan int 可以隐转换为 <-chan int
fmt.Println("处理完毕")
}chan<- T 表示只写 channel,<-chan T 表示只读 channel。将 chan T 传给参数时会自动转换方向限制。这种设计让函数签名清楚地表达了 channel 的使用意图。
close() 函数
close 关闭 channel,关闭后不能再发送数据,但可以继续接收。
package main
import "fmt"
func main() {
ch := make(chan int, 3)
// 发送数据
ch <- 1
ch <- 2
ch <- 3
close(ch) // 关闭 channel
// 关闭后仍可接收
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
// 缓冲区空后继续接收,返回零值
val, ok := <-ch
fmt.Printf("val=%d, ok=%v\n", val, ok) // val=0, ok=false
// 关闭后发送会 panic
// ch <- 4 // panic: send on closed channel
// 检查 channel 是否关闭
if !ok {
fmt.Println("channel 已关闭且为空")
}
}关闭 channel 的规则:
- 只能由发送方关闭,不能由接收方关闭
- 关闭后不能发送,但可以接收剩余数据
- 重复关闭会 panic
- 向 nil channel 发送或接收会永久阻塞
range 遍历 channel
for range 可以持续从 channel 接收数据,直到 channel 被关闭。
package main
import "fmt"
func fibonacci(ch chan int, n int) {
a, b := 0, 1
for i := 0; i < n; i++ {
ch <- a
a, b = b, a+b
}
close(ch) // 必须关闭,否则 range 会永久阻塞
}
func main() {
ch := make(chan int)
go fibonacci(ch, 10)
// range 会自动接收直到 channel 关闭
fmt.Print("斐波那契数列: ")
for val := range ch {
fmt.Printf("%d ", val)
}
fmt.Println()
}for val := range ch 等价于手动检查 ok 值的循环,代码更简洁。
nil channel 的行为
未初始化的 channel(值为 nil)在读写时都会永久阻塞。
package main
import (
"fmt"
"time"
)
func main() {
var ch chan int // nil channel
// 向 nil channel 发送会永久阻塞
go func() {
ch <- 1
fmt.Println("这行不会执行")
}()
// 从 nil channel 接收也会永久阻塞
go func() {
<-ch
fmt.Println("这行也不会执行")
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("主 goroutine 继续执行")
// nil channel 的常见用途:在 select 中禁用某个 case
readCh := make(chan int, 1)
readCh <- 42
var writeCh chan int // nil,暂时禁用写入
select {
case val := <-readCh:
fmt.Println("读取:", val)
case writeCh <- 1:
fmt.Println("这行不会执行(writeCh 是 nil)")
}
}nil channel 在 select 语句中很有用——将某个 channel 设为 nil 可以临时禁用对应的 case 分支。
Channel 的数据流可以这样理解:发送方通过 <- 将数据放入管道,接收方通过 <- 从管道取出数据。无缓冲 channel 要求发送和接收同步就绪,有缓冲 channel 允许数据暂存。多个 goroutine 可以安全地向同一个 channel 发送或接收数据,channel 内部通过互斥锁保证并发安全。
实用模式
下面展示几个 channel 的常见使用模式。
package main
import (
"fmt"
"sync"
"time"
)
// 模式 1:信号通知
func signalPattern() {
done := make(chan struct{})
go func() {
fmt.Println("后台任务执行中...")
time.Sleep(100 * time.Millisecond)
fmt.Println("后台任务完成")
close(done) // 发送完成信号
}()
<-done // 等待信号
fmt.Println("主程序继续")
}
// 模式 2:数据管道
func pipelinePattern() {
// 生成数据
nums := make(chan int)
go func() {
for i := 1; i <= 5; i++ {
nums <- i
}
close(nums)
}()
// 平方处理
squares := make(chan int)
go func() {
for n := range nums {
squares <- n * n
}
close(squares)
}()
// 收集结果
for s := range squares {
fmt.Printf("%d ", s)
}
fmt.Println()
}
// 模式 3:扇出 - 多个 goroutine 从同一 channel 读取
func fanOutPattern() {
jobs := make(chan int, 10)
var wg sync.WaitGroup
// 启动 3 个 worker
for w := 1; w <= 3; w++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d 处理任务 %d\n", workerID, job)
time.Sleep(50 * time.Millisecond)
}
}(w)
}
// 发送 6 个任务
for j := 1; j <= 6; j++ {
jobs <- j
}
close(jobs)
wg.Wait()
fmt.Println("所有任务处理完毕")
}
func main() {
fmt.Println("=== 信号通知模式 ===")
signalPattern()
fmt.Println("\n=== 管道模式 ===")
pipelinePattern()
fmt.Println("\n=== 扇出模式 ===")
fanOutPattern()
}总结
Channel 是 Go 并发通信的核心。无缓冲 channel 实现同步通信,有缓冲 channel 允许异步操作。<- 操作符用于发送和接收,close() 关闭 channel。通过 channel 方向限制(chan<-、<-chan)可以增强类型安全。for range 可以简洁地遍历 channel。nil channel 在 select 中可用于禁用分支。掌握 channel 的各种模式(信号通知、管道、扇出/扇入)是编写高效并发程序的关键。