Go: Go Channel 通信

Channel 是 Go 并发模型的核心原语——"不要通过共享内存来通信,而应通过通信来共享内存。"channel 让 goroutine 之间的数据传递像管道一样安全优雅。

如果说 goroutine 是 Go 并发的"人",channel 就是他们之间的"电话线"。这节课你将掌握 channel 的全部核心用法和常见陷阱。

1. 你将学到


2. 一个后端工程师的真实故事

(1) 痛点:共享内存 + Mutex 代码又丑又容易死锁

Alice 是支付团队的后端工程师,她需要在多个 goroutine 之间传递交易数据:

"我用 sync.Mutex 保护一个共享的 []Transaction slice,5 个 goroutine 往里写、3 个 goroutine 往外读。代码 100 行里 30 行是 Lock/Unlock,reviewer 看了直接说'这代码我 review 不了一行都不敢动'。"

她的代码像这样:

GO
// 共享内存方式:读写都加锁,代码难以维护
type TransactionPool struct {
    mu     sync.Mutex
    items  []Transaction
}

func (p *TransactionPool) Add(t Transaction) {
    p.mu.Lock()
    defer p.mu.Unlock()
    p.items = append(p.items, t)
}

func (p *TransactionPool) Get() Transaction {
    p.mu.Lock()
    defer p.mu.Unlock()
    if len(p.items) == 0 {
        return Transaction{}
    }
    item := p.items[0]
    p.items = p.items[1:]
    return item
}

(2) Go 的解法:channel 传递数据

GO
// channel_approach.go
package main

import "fmt"

type Transaction struct {
    ID     string
    Amount float64
}

func main() {
    // 创建一个无缓冲 channel
    ch := make(chan Transaction)

    // 生产者 goroutine
    go func() {
        tx := Transaction{ID: "TXN-001", Amount: 99.99}
        ch <- tx  // 发送(阻塞直到接收方就绪)
        fmt.Println("生产者: 发送完成")
    }()

    // 消费者
    tx := <-ch  // 接收(阻塞直到发送方就绪)
    fmt.Printf("消费者: 收到 %s ($%.2f)\n", tx.ID, tx.Amount)
}

输出:

TEXT 📖 仅展示
消费者: 收到 TXN-001 ($99.99)
生产者: 发送完成

(3) 收益:channel vs 共享内存

维度 共享内存 + Mutex Channel
代码量 30 行模板代码 1 行 ch <- v
线程安全 手动 Lock/Unlock 语言内置保证
耦合度 高(共享变量耦合) 低(只依赖 chan interface)
可测试性 需要 mock Mutex 直接用 channel 测试
死锁风险 容易(Lock 顺序问题) 编译期可发现部分死锁
💡 提示: Go 谚语:"不要通过共享内存来通信,而应通过通信来共享内存。"——不共享数据,而是把数据通过 channel 从一个 goroutine 传到另一个,每个数据只有一个拥有者。


3. channel 创建与基本操作

(1) 创建 channel

GO
package main

import "fmt"

func main() {
    // 无缓冲 channel(同步)
    ch1 := make(chan int)

    // 有缓冲 channel(异步,容量 3)
    ch2 := make(chan string, 3)

    // nil channel(不能直接使用)
    var ch3 chan float64

    fmt.Printf("ch1: %T, 无缓冲\n", ch1)
    fmt.Printf("ch2: %T, 缓冲=%d\n", ch2, cap(ch2))
    fmt.Printf("ch3: %T, nil=%v\n", ch3, ch3 == nil)
}

(2) 发送与接收

GO
ch := make(chan int)

// 发送:ch <- value
ch <- 42  // 无缓冲:阻塞直到有人接收

// 接收:value := <-ch
value := <-ch  // 阻塞直到有人发送

// 忽略接收值
<-ch  // 只接收不处理(用于同步信号)

▶ 示例:无缓冲 channel 的同步行为

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)

    go func() {
        fmt.Println("goroutine: 准备发送...")
        ch <- "hello"  // 阻塞直到 main 接收
        fmt.Println("goroutine: 发送完成")
    }()

    time.Sleep(1 * time.Second)  // 模拟延迟
    fmt.Println("main: 准备接收...")
    msg := <-ch
    fmt.Printf("main: 收到 %q\n", msg)
}
▶ 试一试

输出:

TEXT 📖 仅展示
goroutine: 准备发送...
main: 准备接收...
goroutine: 发送完成
main: 收到 "hello"
🔥 易错(无缓冲 channel 同步): 发送和接收必须同时就绪,否则一方会阻塞。无缓冲 channel = 同步——发送方等待接收方,接收方等待发送方,两者"握手"后同时继续。


4. 无缓冲 vs 有缓冲 channel

(1) 有缓冲 channel 的异步行为

GO
package main

import (
    "fmt"
    "time"
)

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

    go func() {
        for i := 1; i <= 5; i++ {
            ch <- i
            fmt.Printf("发送 %d (len=%d)\n", i, len(ch))
        }
        close(ch)
    }()

    time.Sleep(500 * time.Millisecond)

    for v := range ch {
        fmt.Printf("接收 %d\n", v)
        time.Sleep(200 * time.Millisecond)
    }
}

输出:

TEXT 📖 仅展示
发送 1 (len=0)
发送 2 (len=1)
发送 3 (len=2)
发送 4 (len=3)
发送 5 (len=4)
接收 1
接收 2
接收 3
接收 4
接收 5

▶ 示例:缓冲满时阻塞

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 2)  // 容量 2

    ch <- 1  // ✅ 不阻塞(有空间)
    ch <- 2  // ✅ 不阻塞(有空间)
    // ch <- 3  // ❌ 阻塞!缓冲满了,等待接收方

    go func() {
        time.Sleep(100 * time.Millisecond)
        fmt.Println("goroutine: 开始接收")
        <-ch  // 释放一个位置
    }()

    fmt.Println("main: 发送 3")
    ch <- 3  // 现在不阻塞了(goroutine 已接收了一个)
    fmt.Println("main: 发送完成")
    close(ch)

    for v := range ch {
        fmt.Printf("v=%d\n", v)
    }
}
▶ 试一试

(2) 无缓冲 vs 有缓冲对比

特性 无缓冲 make(chan T) 有缓冲 make(chan T, n)
发送行为 阻塞直到接收方就绪 缓冲有空位时不阻塞
接收行为 阻塞直到发送方就绪 缓冲有数据时不阻塞
同步/async 同步(握手) async(队列)
容量 0 n
典型场景 同步信号、goroutine 间协调 任务队列、流水线

5. close 与 range iterate

(1) close 关闭 channel

GO
package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    close(ch)  // 关闭后不能再发送

    // 关闭后可以继续接收剩余数据
    v1 := <-ch  // 1
    v2 := <-ch  // 2
    v3 := <-ch  // 0(零值,ok=false)
    fmt.Println(v1, v2, v3)
}

▶ 示例:comma-ok 判断 channel 是否关闭

GO
package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 10
    ch <- 20
    close(ch)

    // comma-ok:ok=false 表示 channel 已关闭且无数据
    v, ok := <-ch
    fmt.Printf("v=%d, ok=%v\n", v, ok)  // v=10, ok=true

    v, ok = <-ch
    fmt.Printf("v=%d, ok=%v\n", v, ok)  // v=20, ok=true

    v, ok = <-ch
    fmt.Printf("v=%d, ok=%v\n", v, ok)  // v=0, ok=false(已关闭且无数据)
}
▶ 试一试

(2) for range 遍历 channel

GO
package main

import "fmt"

func main() {
    ch := make(chan string, 3)
    ch <- "Alice"
    ch <- "Bob"
    ch <- "Charlie"
    close(ch)

    // for range 自动在 channel 关闭后退出
    for name := range ch {
        fmt.Printf("Hello, %s!\n", name)
    }

    // 等价于:
    // for {
    //     name, ok := <-ch
    //     if !ok { break }
    //     fmt.Println(name)
    // }
}
🔥 易错: 向已关闭的 channel 发送数据会 panic。关闭已关闭的 channel 也会 panic。接收方不需要关心关闭——for range 自动处理。永远由发送方负责 close


6. 方向限制

(1) 函数参数指定 channel 方向

GO
package main

import "fmt"

// 只写 channel(只能发送)
func producer(out chan<- int) {
    for i := 1; i <= 3; i++ {
        out <- i  // ✅ 只能发送
    }
    close(out)
}

// 只读 channel(只能接收)
func consumer(in <-chan int) {
    for v := range in {
        fmt.Printf("收到: %d\n", v)  // ✅ 只能接收
    }
}

func main() {
    ch := make(chan int, 3)
    go producer(ch)   // 自动转为 chan<- int
    consumer(ch)       // 自动转为 <-chan int
}

▶ 示例:方向限制实战(管道模式)

GO
package main

import (
    "fmt"
    "strings"
)

// 阶段 1:只写
func stage1(names []string, out chan<- string) {
    for _, name := range names {
        out <- strings.ToUpper(name)
    }
    close(out)
}

// 阶段 2:读写(从 in 读,处理后写到 out)
func stage2(in <-chan string, out chan<- string) {
    for name := range in {
        out <- "Hello, " + name + "!"
    }
    close(out)
}

// 阶段 3:只读
func stage3(in <-chan string) {
    for msg := range in {
        fmt.Println(msg)
    }
}

func main() {
    ch1 := make(chan string, 3)
    ch2 := make(chan string, 3)

    names := []string{"Alice", "Bob", "Charlie"}

    go stage1(names, ch1)
    go stage2(ch1, ch2)
    stage3(ch2)
}
▶ 试一试

输出:

TEXT 📖 仅展示
Hello, ALICE!
Hello, BOB!
Hello, CHARLIE!

(2) 方向限制用法

declare 权限 用途
ch chan T 读写 变量声明
ch chan<- T 只写 生产者函数参数
ch <-chan T 只读 消费者函数参数
💡 提示: 方向限制在编译期检查——把双向 channel 传给只写参数时不会报错(自动隐式转换),但如果想在只写 channel 上接收会在编译期报错。


7. select 多路复用入门

(1) select 基础

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "来自 ch1"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "来自 ch2"
    }()

    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case msg := <-ch2:
        fmt.Println(msg)
    case <-time.After(300 * time.Millisecond):
        fmt.Println("超时")
    }
}
💡 提示: select 像 switch 但用于 channel——哪个 case 的 channel 先就绪就执行哪个。如果多个同时就绪,随机选一个。select 是 Go 并发编程的王牌武器,第 15 课会深入讲解。

▶ 示例:select 实现超时控制

GO
package main

import (
    "fmt"
    "time"
)

func longOperation(result chan<- string) {
    time.Sleep(3 * time.Second)
    result <- "完成"
}

func main() {
    result := make(chan string)

    go longOperation(result)

    select {
    case res := <-result:
        fmt.Println(res)
    case <-time.After(1 * time.Second):
        fmt.Println("操作超时!")
    }
}
▶ 试一试

输出:

TEXT 📖 仅展示
操作超时!

8. 常见死锁场景

GO
// 死锁 1:无缓冲 channel 只有发送没有接收
func deadlock1() {
    ch := make(chan int)
    ch <- 42  // fatal error: all goroutines are asleep - deadlock!
}

// 死锁 2:main goroutine 自己等自己
func deadlock2() {
    ch := make(chan int)
    <-ch  // fatal error: deadlock
}

// 死锁 3:多个 goroutine 互相等
func deadlock3() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        <-ch1  // 等 ch1
        ch2 <- 1
    }()

    <-ch2  // 等 ch2
    ch1 <- 1  // 永远到不了这行
}

(2) 死锁 4 种场景速查

场景 原因 解决
只有发送无接收 无缓冲 channel 阻塞 确保有消费者
只有接收无发送 channel 为空且无发送方 确保有生产者
goroutine 互相等待 A 等 B 的 ch,B 等 A 的 ch 重新设计依赖顺序
nil channel 操作 向/从 nil channel 永久阻塞 初始化 channel

9. 完整示例:生产者-消费者管道

GO
// pipeline.go
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// 工作单元
type Job struct {
    ID      int
    Payload string
}

type Result struct {
    Job     Job
    Output  string
    Err     error
    Duration time.Duration
}

// 生产者:生成任务(只写 channel)
func producer(jobs chan<- Job, count int) {
    for i := 1; i <= count; i++ {
        jobs <- Job{
            ID:      i,
            Payload: fmt.Sprintf("task-%d", i),
        }
        time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
    }
    close(jobs)
}

// 消费者:处理任务(读 jobs,写 results)
func consumer(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        start := time.Now()

        // 模拟处理
        time.Sleep(time.Duration(50+rand.Intn(100)) * time.Millisecond)
        output := fmt.Sprintf("consumer-%d processed %s", id, job.Payload)

        results <- Result{
            Job:      job,
            Output:   output,
            Duration: time.Since(start),
        }
    }
}

// 收集器:收集并打印结果(只读 channel)
func collector(results <-chan Result, done chan<- struct{}) {
    var total time.Duration
    count := 0

    for r := range results {
        count++
        total += r.Duration
        status := "OK"
        if r.Err != nil {
            status = "ERR"
        }
        fmt.Printf("[%s] Job#%d: %s (%v)\n",
            status, r.Job.ID, r.Output, r.Duration)
    }

    if count > 0 {
        fmt.Printf("\n共 %d 个任务, 平均 %.2f ms\n",
            count, float64(total.Milliseconds())/float64(count))
    }
    close(done)
}

func main() {
    const (
        jobCount     = 20
        workerCount  = 3
    )

    jobs := make(chan Job, 10)
    results := make(chan Result, 10)
    done := make(chan struct{})

    // 1 个生产者
    go producer(jobs, jobCount)

    // N 个消费者
    var wg sync.WaitGroup
    for w := 1; w <= workerCount; w++ {
        wg.Add(1)
        go consumer(w, jobs, results, &wg)
    }

    // 等待所有消费者完成,然后关闭 results
    go func() {
        wg.Wait()
        close(results)
    }()

    // 收集器(main goroutine)
    collector(results, done)
    <-done
}

预期输出:

TEXT 📖 仅展示
[OK] Job#1: consumer-2 processed task-1 (85ms)
[OK] Job#2: consumer-1 processed task-2 (92ms)
[OK] Job#3: consumer-3 processed task-3 (78ms)
...
[OK] Job#20: consumer-1 processed task-20 (112ms)

共 20 个任务, 平均 87.45 ms
100%
sequenceDiagram
    participant P as Producer
    participant C as Channel
    participant W as Consumer
    
    Note over P: 发送方负责 close
    P->>C: ch <- job (发送)
    activate C
    Note over C: 无缓冲:同步等待<br/>有缓冲:入队
    C->>W: job := <-ch (接收)
    deactivate C
    Note over W: 处理任务
    W-->>P: 继续循环
    Note over P: close(ch)
    Note over C: 剩余数据可读
    Note over W: for range 自动退出
🔥 易错: 本示例中有 3 个 channel(jobs/results/done)和 3 种方向限制(只写/只读/读写)。方向限制在编译期保证你不会在生产者里接收数据——这是 Go 类型系统对并发安全的设计。


❓ 常见问题

Q 无缓冲和有缓冲 channel 有什么区别?
A 无缓冲 channel 发送和接收必须同时就绪(同步握手),否则阻塞。有缓冲 channel 在缓冲未满时发送不阻塞,缓冲非空时接收不阻塞。无缓冲用于同步协调,有缓冲用于异步队列。
Q channel 关闭后还能发送吗?
A 不能。向已关闭的 channel 发送会 panic。关闭后可以继续接收剩余数据,数据读完返回零值。v, ok := <-ch 的 ok=false 表示已关闭且无数据。
Q 方向限制 chan<- 和 <-chan 有什么用?
A 在函数签名中限制 channel 方向——编译期保证生产者不会意外接收、消费者不会意外发送。Go 编译器会在编译时报错,而不是运行时 panic。
Q channel 的底层实现是什么?
A channel 底层是一个循环队列(ring buffer)+ 两个等待队列(sender 队列和 receiver 队列)。发送时如果接收队列有等待者,直接传递;否则放入缓冲或发送队列。Go 源码 runtime/chan.gohchan 结构体。
Q select 怎么用 channel?
A select 同时监听多个 channel,哪个先就绪执行哪个 case。如果多个同时就绪随机选一个。所有 channel 都未就绪且有 default 则执行 default,否则阻塞等待。select 是超时控制和非阻塞操作的关键。
Q 如何避免 channel 死锁?
A 四条规则:无缓冲 channel 必须有对应的接收方;注意 goroutine 之间的依赖顺序;select + default 实现非阻塞操作;用 close 通知接收方结束。
Q channel 可以当信号量用吗?
A 可以。有缓冲 channel 的容量可以当作信号量:初始化 sem := make(chan struct{}, 10),操作前 sem <- struct{}{},完成后 <-sem。缓冲满时阻塞——自然限流。
Q 什么时候该用 channel,什么时候该用 Mutex?
A 数据传递 → channel;数据保护 → Mutex。具体:传递所有权(数据从一个 goroutine 到另一个)→ channel;保护共享状态(多个 goroutine 访问同一变量)→ Mutex。两者不是替代关系,是互补。

📖 小节


📝 作业

  1. 基础题(难度⭐):写一个程序:启动一个 goroutine 发送 0~9 十个数字到无缓冲 channel,main goroutine 接收并打印。观察发送和接收的交替顺序。

  2. 进阶题(难度⭐⭐):实现一个扇出(fan-out)模式:一个生产者发送 100 个任务到 channel,启动 5 个消费者 goroutine 从同一个 channel 读取处理,每个消费者打印自己的 ID 和任务编号。要求用有缓冲 channel + close + for range。

  3. 挑战题(难度⭐⭐⭐):实现一个三阶段管道(pipeline):阶段 1 生成随机数字([]int);阶段 2 过滤出偶数;阶段 3 计算平方和。每个阶段是一个独立的 goroutine,用 channel 连接。要求:(1) 各阶段用方向限制;(2) 支持动态调整阶段数;(3) 用 select 实现优雅退出。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏