Files
cs-note/hzh/GolangStar/Go语言进阶/Channel.md
T

6.7 KiB
Raw Blame History

tags, create time
tags create time
go
golang
Channel
并发
CSP
2026-06-07 14:40

Channel

概述

Channel 是 Go 中 goroutine 之间通信的核心机制。Go 倡导"以通信共享内存,而非共享内存来通信",channel 正是这一哲学(CSP 模型)的具象化体现。本文涵盖 channel 的创建、操作、单向/双向类型、缓冲语义以及实战技巧。

正文

什么是 Channel?

[!question] 💭 思考 如果多个 goroutine 需要交换数据,除了用锁保护共享变量,还有没有更安全的方式?

官方定义:Channel 是一个类型化的管道,通过它你可以发送和接收值。

ch := make(chan int) // 创建一个传递 int 的 channel
ch <- 42             // 发送:向 ch 写入 42
v := <-ch            // 接收:从 ch 读取值到 v

[!info] ℹ️ 核心理念 "Don't communicate by sharing memory; share memory by communicating." — Rob Pike 与其用锁保护共享数据,不如让数据在 goroutine 之间流动——拥有数据的 goroutine 就是唯一能修改它的那个。

创建与初始化

// 无缓冲 channel(同步模式)
ch := make(chan int)

// 有缓冲 channel(异步模式,容量为 3)
ch := make(chan int, 3)

[!warning] ⚠️ 常见错误

  • 未初始化的 channel 是 nil,对 nil channel 发送/接收会永久阻塞
  • 重复关闭 channel 会 panic
  • 向已关闭的 channel 发送会 panic

基本操作

操作 语法 说明
发送 ch <- value 向 channel 写入,可能阻塞
接收 <-ch 从 channel 读取,可能阻塞
带值接收 v := <-ch 读取值并赋值
关闭 close(ch) 标记 channel 不再发送数据
package main

import "fmt"

func main() {
    ch := make(chan int)
    
    go func() {
        ch <- 42   // 生产者:发送数据
        close(ch)  // 生产完毕,关闭 channel
    }()
    
    v := <-ch // 消费者:接收数据
    fmt.Println(v) // 42
}

关闭 Channel 后的行为

[!question] 💭 思考 关闭一个 channel 后还能读吗?还能写吗?读完已有数据后再读会怎样?

操作 未关闭 已关闭(有数据) 已关闭(空)
发送 ✅ 正常 ❌ panic ❌ panic
接收 ✅ 正常 ✅ 返回已有数据 ✅ 返回零值
关闭 ✅ 正常 ✅ 可重复关闭检查 ❌ panic(重复关闭)
ch := make(chan int, 5)
ch <- 1
close(ch)

// 关闭后仍可读取
for i := 0; i < 5; i++ {
    v := <-ch
    fmt.Println(v) // 第1次: 1, 之后全是 0
}

安全读取:ok 模式

当需要从已关闭的 channel 区分"有数据"和"已关闭"时,使用多返回值形式:

v, ok := <-ch
if ok {
    fmt.Println("收到数据:", v)
} else {
    fmt.Println("channel 已关闭,无更多数据")
}

for-range 读取

这是最优雅的 channel 消费方式——自动在 channel 关闭时退出循环:

go func() {
    for v := range ch { // channel 关闭后自动退出
        fmt.Println("收到:", v)
    }
    fmt.Println("channel 已关闭,循环结束")
}()

close(ch) // 触发 for-range 退出

[!tip] 💡 最佳实践 发送端负责关闭,接收端不负责关闭。如果一个 channel 有多个发送方,谁该关闭它?这种模糊场景应尽量避免。

有缓冲 vs 无缓冲

flowchart LR
    subgraph Unbuf["无缓冲 channel"]
        S[发送方] -->|"阻塞直到接收方就绪"| R[接收方]
    end
    
    subgraph Buf["有缓冲 channel (cap=3)"]
        B["缓冲区 📦📦"] -.->|剩余容量| S2[发送方]
        R2[接收方] -.->|有空位| S2
        R2 -->|"取出"| B
    end
  • 无缓冲 channel:发送和接收同步发生——发送方阻塞直到接收方准备好,反之亦然。适用于需要严格配对的生产者-消费者场景。
  • 有缓冲 channel:发送方在缓冲区未满时无需等待即可返回;接收方在缓冲区非空时无需等待即可拿到数据。适用于解耦生产速率和消费速率的场景。

[!warning] ⚠️ 注意 缓冲区满了以后,有缓冲 channel 也会退化为同步模式——发送方将被阻塞。长期满队列意味着消费者跟不上生产者的节奏,应考虑增加消费者数量或扩大缓冲区。

单向 Channel

有时我们希望限制 channel 的使用方向,比如在函数签名中明确表达"这个参数只用于发送"或"只用于接收":

// 只发送 channel —— 只能 ch<-value
sendCh := make(chan int)
var sendOnly chan<- int = sendCh

// 只接收 channel —— 只能 <-ch
var recvOnly <-chan int = sendCh
func producer(out chan<- int) {
    for i := 0; i < 5; i++ {
        out <- i // 只能发,不能收
    }
    close(out)
}

func consumer(in <-chan int) {
    for v := range in { // 只能收,不能发
        fmt.Println("received:", v)
    }
}

[!note] 📝 关键规则 只有接收方才能关闭 channel。单向 channel 的类型转换是单向的:双向可以转为单向,单向不能转回双向。

经典模式:扇入扇出

[!question] 💭 思考 如果有 10 个任务要并行处理,但结果需要汇总到一个地方,怎么设计?

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("worker %d processing job %d\n", id, j)
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // 启动 3 个 worker(扇出)
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // 发送任务
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // 等待所有 worker 完成
    close(results)
    for r := range results {
        fmt.Println("result:", r)
    }
}

Channel 实现互斥锁

[!question] 💭 思考 channel 本身已经是并发安全的了,能不能利用这一点来代替 mutex?

一个容量为 1 的 channel 可以充当信号量:

ch := make(chan struct{}, 1)
ch <- struct{}{} // 获取"锁"

// 临界区:同时只有一个 goroutine 能执行
*counter++

<-ch // 释放"锁"

[!tip] 💡 何时用 Channel vs Mutex?

  • 优先用 channel:goroutine 间的数据传递、事件通知、生命周期管理
  • 优先用 mutex:保护共享变量的并发访问
  • 两者不互斥,复杂场景中经常配合使用

关联笔记