This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/DEV/GO/Channel详解.md
T

8.5 KiB
Raw Blame History

tags, create time
tags create time
go
golang
runtime
channel
并发
sync
2026-04-24 10:00

Channel 详解

概述

Channel 是 Go 中最核心的并发原语之一,遵循 "不要通过共享内存来通信,而是通过通信来共享内存" 的设计哲学。它是 goroutine 之间传递数据的同步机制,也是 Go 并发编程的灵魂。

思考:既然有 Mutex 可以做同步,为什么还要发明 Channel?Channel 相比 Mutex 的抽象层次有什么优势? 详见 DEV/GO/Channel vs Mutex。

Channel 的本质是一个线程安全的 FIFO 队列,支持三种操作:发送、接收、关闭。

Channel 的基础类型

// 声明三种 channel
var ch1 chan int            // nil channel(未初始化)
var ch2 chan int = make(chan int)         // 无缓冲 channel
var ch3 chan int = make(chan int, 10)     // 有缓冲 channel,容量 10

无缓冲 Channel

无缓冲 channel 的 len() == 0、cap() == 0。

核心语义:发送和接收必须同时完成,是同步的。

func main() {
    ch := make(chan int)  // 无缓冲

    go func() {
        ch <- 42          // 发送:如果没人接收,会阻塞
        fmt.Println("sent 42")
    }()

    val := <-ch           // 接收:如果没人发送,会阻塞
    fmt.Println("received:", val)  // 输出: received: 42
}

无缓冲 channel 的行为图解:

sequenceDiagram
    participant Sender as 发送方 goroutine
    participant Channel as 无缓冲 channel
    participant Receiver as 接收方 goroutine

    Sender->>Channel: ch <- 42
    Note over Sender,Channel: 双方同时就绪
    Channel->>Receiver: 传递 42
    Receiver->>Channel: <-ch 接收完成
    Note over Receiver,Channel: 发送和接收同时完成
    Sender->>Sender: 继续执行
    Receiver->>Receiver: 继续执行

关键点:无缓冲 channel 实现了 goroutine 之间的同步屏障。发送方阻塞到接收方 ready,接收方阻塞到发送方 ready。

有缓冲 Channel

有缓冲 channel 的 len() 是当前元素数量,cap() 是缓冲区容量。

核心语义:缓冲区未满时发送不阻塞,缓冲区非空时接收不阻塞。

func main() {
    ch := make(chan int, 3)  // 缓冲容量 3

    ch <- 1  // len=0→1, 不阻塞(缓冲区有空位)
    ch <- 2  // len=1→2, 不阻塞
    ch <- 3  // len=2→3, 不阻塞(缓冲区满)

    fmt.Println(len(ch))  // 输出: 3
    fmt.Println(cap(ch))  // 输出: 3

    // 缓冲区已满,下一条发送会阻塞
    // ch <- 4  // ❌ 阻塞!

    val := <-ch  // len=3→2, 不阻塞
    fmt.Println("received:", val)  // 输出: received: 1
    fmt.Println(len(ch))  // 输出: 2
}

缓冲区的内部结构:

graph LR
    subgraph Buffer["channel 缓冲区 (cap=4)"]
        B1[元素 1]
        B2[元素 2]
        B3[元素 3]
        B4[空位]
    end

    subgraph Send["发送方"]
        S["ch <- 4"]
    end

    subgraph Recv["接收方"]
        R["<- ch"]
    end

    S -->|"缓冲区未满,直接写入"| Buffer
    Buffer -->|"缓冲区非空,直接读取"| R

    classDef buf fill:#e3f2fd,stroke:#1565c0
    classDef send fill:#fff3e0,stroke:#e65100
    classDef recv fill:#e8f5e9,stroke:#2e7d32
    class Buffer buf
    class S send
    class R recv

len() vs cap()

属性 含义 示例
len(ch) 当前缓冲区中的元素数量 0 ~ cap 之间变化
cap(ch) 缓冲区的最大容量 创建时确定,不可改变

思考:len() 和 cap() 分别在什么时机更新?如果一个 goroutine 在发送,另一个在接收,len() 会正确反映当前值吗?

Channel 的关闭

ch := make(chan int, 3)

ch <- 1
ch <- 2

close(ch)  // 关闭 channel

// 关闭后还能接收已发送的数据
val, ok := <-ch  // val=1, ok=true
val, ok := <-ch  // val=2, ok=true
val, ok := <-ch  // val=0 (零值), ok=false  // 缓冲区空了,收到零值

// 从关闭的 channel 接收,ok 始终为 false
val, ok := <-ch  // val=0, ok=false

close() 的规则:

操作 合法? 后果
close(ch) — 正常关闭 ✅ 接收方可以读完残留数据,后续接收返回零值和 false
向已关闭的 channel 发送 ❌ panic: send on closed channel
关闭 nil channel ❌ panic: close of nil channel
关闭已关闭的 channel ❌ panic: close of closed channel

核心原则:发送方负责关闭 channel,接收方不负责关闭。 多个发送方场景下,建议用 sync.Once 确保只关闭一次。

nil Channel

nil channel 是一个未初始化的 channel(值为 nil)。

var ch chan int  // nil channel

// 向 nil channel 发送 → 永远阻塞
// ch <- 1  // ❌ 死锁

// 从 nil channel 接收 → 永远阻塞
// <-ch  // ❌ 死锁

// 关闭 nil channel → panic
// close(ch)  // ❌ panic: close of nil channel

nil channel 的实际用途:用于 select 中禁用某个分支。

func main() {
    ch1 := make(chan int)
    var ch2 chan int  // nil

    select {
    case v := <-ch1:
        fmt.Println("received from ch1:", v)
    case <-ch2:
        fmt.Println("from ch2 (never reaches here)")
    }
    // select 永远阻塞在 ch1 上,ch2 分支被静默禁用
}

思考:为什么 nil channel 是"永远阻塞"而不是"立即返回"?这体现了 Go 对 nil 的哪种设计哲学?

Channel 死锁场景

场景 1:向无人读的 channel 写

func main() {
    ch := make(chan int)
    ch <- 42  // 阻塞:没有接收方
    // 输出: all goroutines are asleep - deadlock!
}

场景 2:读无人写的 channel

func main() {
    ch := make(chan int)
    <-ch  // 阻塞:没有发送方
    // 输出: all goroutines are asleep - deadlock!
}

场景 3:读写 nil channel

func main() {
    var ch chan int  // nil
    <-ch  // 永远阻塞 → 死锁
}

场景 4:goroutine 中读写已关闭的 channel

func main() {
    ch := make(chan int)
    close(ch)

    go func() {
        ch <- 1  // panic: send on closed channel
    }()

    <-ch  // 正常接收
}

for-range 与 Channel

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    close(ch)  // 必须关闭,for-range 才知道何时结束

    for v := range ch {  // 自动读取直到 channel 关闭且空
        fmt.Println(v)   // 输出: 1, 2
    }
}

思考:如果不对 channel 调用 close(),for range 会发生什么?这与无缓冲 channel 的死锁有什么区别?

常见模式

Worker Pool(工作池)

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

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 <= 5; j++ {
        jobs <- j
    }
    close(jobs)  // 发送完毕后关闭

    // 等待结果
    for i := 1; i <= 5; i++ {
        <-results
    }
}

超时控制

select {
case v := <-ch:
    fmt.Println("received:", v)
case <-time.After(5 * time.Second):
    fmt.Println("timed out after 5s")
}

Channel 内部实现要点

// Go 源码简化版 channel 结构
type hchan struct {
    buf      unsafe.Pointer  // 缓冲区(有缓冲时)
    elemsize uint16          // 每个元素的大小
    closed   uint32          // 是否关闭
    elemtype *type           // 元素类型
    sendx    uint            // 发送索引
    recvx    uint            // 接收索引
    recvq    waitq           // 等待接收的 goroutine 队列
    sendq    waitq           // 等待发送的 goroutine 队列

    lock mutex  // 保护所有字段的互斥锁
}
  • Channel 是并发安全的,内部使用 mutex 保护所有操作
  • 无缓冲 channel:直接 sender ↔ receiver 握手(锁粒度极小)
  • 有缓冲 channel:先写入缓冲区(buf 循环缓冲区),满时才阻塞
  • recvq 和 sendq 是 Goroutine 等待队列(与 GMP 模型联动)

关键理解:Channel 的锁只保护缓冲区操作本身,不影响 goroutine 的调度——GMP 模型负责调度等待队列中的 goroutine。

关联笔记