channel

·go
#fundamentals

在 Go 语言里,channel(通道)是核心的并发原语之一,用于在不同的 goroutine 之间安全地传递数据

它不仅能让你在并发程序中避免显式的锁和条件变量,还能让代码更具可读性和可维护性

基础概念

什么是 channel

Channel 本质上是一个管道,它可以将一个 goroutine 产生的数据,安全地传递给另一个(或多个)goroutine

通过 channel,数据的发送(send)和接收(receive)是同步的操作——未被接收前,发送就会阻塞;未有数据可接收时,接收也会阻塞

设计思想

Go 的并发模型强调“不要通过共享内存来通信,而应通过通信来共享内存”(Don’t communicate by sharing memory; share memory by communicating),Channel 正是实现这一理念的手段

创建与基本操作

创建 channel
// 最常见的方式:make
ch := make(chan int)          // 创建一个无缓冲的 int 类型通道
ch2 := make(chan string, 10)  // 创建一个缓冲容量为 10 的 string 类型通道

无缓冲通道make(chan T)):发送和接收都是同步阻塞

缓冲通道make(chan T, N),N>0):可以在缓冲区未满时,发送者无需等待接收者即可发送;只有当缓冲区满,才会阻塞发送

发送与接收

// 发送:ch <- value
go func() {
    ch <- 42
}()

// 接收:value := <- ch
v := <-ch
fmt.Println("收到:", v)

接收并丢弃<-ch,如果只是为了同步,无需保存数据

非阻塞尝试:用带 ok 的接收形式:

if v, ok := <-ch; ok {
    // 通道未关闭,v 是有效值
} else {
    // 通道已关闭,读取不到更多数据
}

通道方向与类型

Go 支持将通道声明为单向,以更严格地限制读写权限:

func producer(out chan<- int) {    // 只能写
    out <- 1
}

func consumer(in <-chan int) {     // 只能读
    fmt.Println(<-in)
}

chan<- T:只能向通道发送数据

<-chan T:只能从通道接收数据

使用单向通道,可以在函数签名里更清晰地表达意图,减少误用

关闭通道与遍历

关闭通道
close(ch)

关闭后,不能再发送,否则会 panic

但仍可继续接收,接收到缓冲区数据后,再接收将不断拿到类型零值,并且 okfalse

谁来关闭通道有约定俗成的做法:发送方负责关闭,接收方只用来读取

遍历通道
go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}()

for v := range ch {
    fmt.Println("遍历收到:", v)
}

for v := range ch 会在通道关闭并且所有数据读完后自动退出循环

select 多路复用

select 能够让你同时等待多个通道操作,就像 switch 一样,但分支条件是通道的发送或接收是否就绪

select {
case v := <-ch1:
    fmt.Println("从 ch1 收到", v)
case ch2 <- 10:
    fmt.Println("向 ch2 发送了 10")
case <-time.After(time.Second):
    fmt.Println("超时,无操作")
default:
    fmt.Println("都没有准备好时执行")
}

超时控制:结合 time.After 可以轻松实现带超时的接收

default 分支:当各通道都不就绪时,立即执行 default,避免阻塞

常见并发模式

流水线(Pipeline)

将多个处理阶段串联,每一阶段由一个或多个 goroutine 负责,阶段间通过 channel 传递数据,实现数据流式处理

工作池(Worker Pool)

创建固定数量的 worker goroutine,所有任务发送到一个任务通道,多 worker 并发消费,结合结果通道汇总

这种模式能有效控制并发数量,防止过载

广播(Fan‑outFan‑in

Fan‑out:一个数据源 channel,被多个 goroutine 接收,构成并行处理

Fan‑in:多个 producer 向同一个 channel 发送,消费者统一读取合并结果

使用场景

任务调度与协作:

不同模块或服务间传递任务与结果

事件通知:

比如服务器优雅关闭时,主 goroutine 通知各子任务停止

异步管道处理:

日志收集、数据过滤、聚合统计,构建高吞吐的流水线

超时与限流:

结合 select 和定时器,优雅处理阻塞、超时、心跳检测

性能与注意事项

阻塞与死锁

确保至少有一方在等待时另一方能够发送或接收,否则程序会在 send/receive 卡住并 panic

在主 goroutinefor range 未关闭通道就退出,会造成死锁

缓冲大小

缓冲太小会频繁阻塞,太大则占用内存且不易察觉阻塞点

根据业务高峰期并发量合理预估

尽量少用全局通道

通道如果过度共享,可能会让控制流变得难以追踪

倾向于将通道作为函数间或模块间的输入输出接口

避免滥用 select{default}

默认分支虽能避免阻塞,但也可能导致“忙等”浪费 CPU

除非确实需要非阻塞检查,否则谨慎使用

最佳实践

清晰的关闭责任:

发送方关闭通道;接收方只负责读取

使用上下文(context.Context)和 select 结合:

在有取消需求的场景中,监听 ctx.Done(),让 goroutine 能及时退出

对方向明确:

在 API 设计中,尽量用单向通道签名,表达只读或只写意图

测试并发场景:

使用 Go 的 race 检测(go test -race)来发现潜在的竞态条件

小结

Go 的 channel 提供了一种简洁、安全的方式来组织并发逻辑

无论是流水线、工作池、超时控制还是事件通知,channel 都能让你更自然地用“管道”思维去设计系统

掌握 channel 的用法和并发模式,不仅能提升代码的可维护性,还能帮助你写出高性能、健壮的并发程序