404 Not Found

404 Not Found


nginx

Channel

14. Channel(通道)

生活类比

想象一条传送带:一端的人把包裹放上去,另一端的人把包裹取下来。

这就是 channel 的本质——goroutine 之间安全通信的管道


核心概念

概念 说明
channel 类型化的通信管道,用 chan 关键字声明
发送 ch <- value 将数据写入通道
接收 value := <-ch 从通道读取数据
无缓冲 channel 同步通信,发送和接收必须同时就绪
有缓冲 channel 异步通信,缓冲区未满时发送不阻塞
close 关闭 channel,不再允许发送
range 从 channel 持续接收直到关闭
方向限制 只发送 chan<- 或只接收 <-chan

Go 的哲学: Don't communicate by sharing memory; share memory by communicating. 不要通过共享内存来通信,而要通过通信来共享内存。


基本语法与用法

创建 channel

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

// 有缓冲 channel(异步,缓冲区大小为 5)
ch := make(chan string, 5)

发送与接收

GO
ch := make(chan int)

// 发送(在另一个 goroutine 中)
go func() {
    ch <- 42 // 将 42 发送到 channel
}()

// 接收
value := <-ch // 从 channel 接收值,阻塞直到有数据
fmt.Println(value) // 42

关闭 channel

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

使用 range 接收

GO
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)

// range 会持续接收直到 channel 关闭
for v := range ch {
    fmt.Println(v) // 10 20 30
}

方向限制

GO
// 只能发送的 channel
func producer(ch chan<- int) {
    ch <- 100
}

// 只能接收的 channel
func consumer(ch <-chan int) {
    v := <-ch
    fmt.Println(v)
}

💡 Tips


示例

示例:基本的 goroutine 通信(难度⭐)

GO
package main

import "fmt"

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

    // 启动 goroutine 发送消息
    go func() {
        ch <- "你好,主线程!"
    }()

    // 主 goroutine 接收消息(阻塞等待)
    msg := <-ch
    fmt.Println(msg) // 你好,主线程!
}
▶ 试一试

说明: 无缓冲 channel 使两个 goroutine 同步——发送方会阻塞直到接收方准备好。


示例:生产者-消费者模式(难度⭐⭐)

GO
package main

import "fmt"

// 生产者:生成数据并发送到 channel(只能发送)
func producer(id int, ch chan<- int, count int) {
    for i := 0; i < count; i++ {
        value := id*100 + i
        fmt.Printf("生产者 %d: 发送 %d\n", id, value)
        ch <- value
    }
}

// 消费者:从 channel 接收数据并处理(只能接收)
func consumer(id int, ch <-chan int) {
    for v := range ch {
        fmt.Printf("消费者 %d: 处理 %d\n", id, v)
    }
    fmt.Printf("消费者 %d: channel 已关闭,退出\n", id)
}

func main() {
    // 创建有缓冲的 channel
    ch := make(chan int, 5)

    // 启动 2 个生产者
    go producer(1, ch, 3)
    go producer(2, ch, 3)

    // 启动 2 个消费者
    go consumer(1, ch)
    go consumer(2, ch)

    // 等待所有生产者完成(实际项目中应使用 sync.WaitGroup)
    // 这里简单用 sleep 演示
    import_time := time.After(2 * time.Second)
    <-import_time

    close(ch) // 关闭 channel

    // 给消费者一点时间处理完剩余数据
    time.Sleep(500 * time.Millisecond)
    fmt.Println("所有工作完成")
}
▶ 试一试

说明:


示例:用 channel 实现信号量与扇出扇入(难度⭐⭐⭐)

GO
package main

import (
    "fmt"
    "sync"
    "time"
)

// worker:处理任务并将结果发送到结果 channel
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Worker %d: 开始处理任务 %d\n", id, job)
        time.Sleep(time.Duration(100+id*50) * time.Millisecond) // 模拟耗时
        result := job * job
        fmt.Printf("Worker %d: 任务 %d 完成,结果 %d\n", id, job, result)
        results <- result
    }
}

func main() {
    numJobs := 10
    numWorkers := 3

    jobs := make(chan int, numJobs)     // 任务 channel
    results := make(chan int, numJobs)  // 结果 channel

    // 启动 worker 池
    var wg sync.WaitGroup
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // 发送任务
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs) // 关闭任务 channel,worker 的 range 循环会结束

    // 等待所有 worker 完成后关闭结果 channel
    go func() {
        wg.Wait()
        close(results)
    }()

    // 收集所有结果
    total := 0
    for r := range results {
        total += r
    }

    fmt.Printf("所有结果之和: %d\n", total)
}
▶ 试一试

说明:


实际应用场景

场景 1:超时控制

GO
package main

import (
    "fmt"
    "time"
)

func slowOperation(ch chan<- string) {
    time.Sleep(3 * time.Second) // 模拟耗时操作
    ch <- "操作完成"
}

func main() {
    ch := make(chan string, 1)
    go slowOperation(ch)

    // 使用 select 实现超时控制
    select {
    case result := <-ch:
        fmt.Println("收到结果:", result)
    case <-time.After(2 * time.Second):
        fmt.Println("操作超时!") // 2 秒后触发
    }
}

说明: time.After 返回一个 channel,在指定时间后发送值。结合 select 可以实现优雅的超时控制。


场景 2:并发爬虫的速率限制

GO
package main

import (
    "fmt"
    "time"
)

// fetchURL 模拟抓取 URL
func fetchURL(url string) string {
    time.Sleep(500 * time.Millisecond) // 模拟网络延迟
    return "页面内容: " + url
}

func main() {
    urls := []string{
        "https://example.com/page1",
        "https://example.com/page2",
        "https://example.com/page3",
        "https://example.com/page4",
        "https://example.com/page5",
    }

    // 用带缓冲的 channel 作为信号量,限制并发数为 2
    semaphore := make(chan struct{}, 2)
    results := make(chan string, len(urls))

    for _, url := range urls {
        go func(u string) {
            semaphore <- struct{}{} // 获取信号量(满了就阻塞)
            fmt.Printf("开始抓取: %s\n", u)
            result := fetchURL(u)
            results <- result
            <-semaphore // 释放信号量
        }(url)
    }

    // 收集结果
    for i := 0; i < len(urls); i++ {
        fmt.Println(<-results)
    }

    fmt.Println("所有页面抓取完成")
}

说明: 带缓冲的 channel 充当信号量,限制同时运行的 goroutine 数量,避免过多并发请求压垮目标服务器。


❓ 常见问题

Q1: 向已关闭的 channel 发送数据会怎样?

会 panic。 任何向已关闭 channel 的发送操作都会触发 panic: send on closed channel。接收操作不会 panic,但会返回零值。

GO
ch := make(chan int, 1)
ch <- 1
close(ch)
// ch <- 2 // ❌ panic: send on closed channel
v := <-ch   // ✅ 返回 1(缓冲区中剩余的值)
v2 := <-ch  // ✅ 返回 0(零值,channel 已空且已关闭)

最佳实践: 由发送方关闭 channel,而不是接收方。如果多个发送方共享一个 channel,使用 sync.Once 或额外的 done channel 来协调关闭。


Q2: nil channel 会怎样?

发送和接收都会永久阻塞,关闭 nil channel 会 panic。

GO
var ch chan int // nil channel

// ch <- 1    // ❌ 永久阻塞
// v := <-ch  // ❌ 永久阻塞
// close(ch)  // ❌ panic: close of nil channel

nil channel 在 select 中很有用——对应的 case 会被自动跳过:

GO
var ch chan int // nil channel
select {
case v := <-ch:       // 被忽略(nil channel)
    fmt.Println(v)
case <-time.After(1 * time.Second):
    fmt.Println("超时")
}

Q3: 无缓冲和有缓冲 channel 的区别?

特性 无缓冲 make(chan T) 有缓冲 make(chan T, n)
发送阻塞条件 接收方未准备好 缓冲区已满
接收阻塞条件 发送方未准备好 缓冲区为空
同步性 同步(会合点) 异步(直到缓冲满)
典型用途 信号通知、同步 生产者-消费者、队列

Q4: 如何判断 channel 是否已关闭?

使用接收操作的第二个返回值:

GO
ch := make(chan int, 1)
ch <- 42
close(ch)

v, ok := <-ch
fmt.Println(v, ok) // 42 true

v2, ok := <-ch
fmt.Println(v2, ok) // 0 false(已关闭且无数据)

okfalse 表示 channel 已关闭且缓冲区中没有剩余数据。


📖 小节

知识点 要点
创建 channel make(chan T) 无缓冲,make(chan T, n) 有缓冲
发送 ch <- value,阻塞条件取决于缓冲类型
接收 value := <-chv, ok := <-ch
关闭 close(ch),由发送方负责,关闭后不能发送
range for v := range ch 持续接收直到关闭
方向限制 chan<- 只发送,<-chan 只接收
select 多路复用,处理多个 channel 操作
最佳实践 优先使用 sync.WaitGroup 等待 goroutine 完成;避免重复关闭

Channel 是 Go 并发编程的核心。 理解无缓冲 vs 有缓冲、close/range、方向限制和 select(下一课),你就掌握了 Go 并发通信的精髓。


📝 作业

练习 1:channel 基础

编写一个程序,启动 3 个 goroutine,每个 goroutine 计算一个数的平方并将结果发送到 channel。主 goroutine 接收并打印所有结果。

要求:


练习 2:用 channel 实现扇入(Fan-in)

编写一个函数 merge(channels ...<-chan int) <-chan int,将多个只接收 channel 合并为一个只接收 channel。

要求:


练习 3:带超时的工作池

实现一个工作池,具有以下功能:

提示: 结合 channel、select 和 time.After 实现。


下一课

15. Select →

Web-Tutorial.com

Web-Tutorial 技术团队

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

100%

🙏 帮我们做得更好

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

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