每日一Go-10、Go语言协程之间的通信:通道Channel介绍

1、通道的定义

通道的定义有3种方式:

1.1 声明但未初始化

go 复制代码
//var ch chan T
//定义一个水管
var waterCh chan string

1.2 无缓冲通道定义:用make()创建

go 复制代码
//ch := make(chan T)
//创建一个水管
waterCh := make(chan string)

无缓冲通道的特定:

  • 写(ch <- v)会阻塞直到有goroutine读(<-ch)
  • 读也会阻塞,直到有写发生
  • 等价于"即时传输",非常适合用于同步,就像握手一样。

1.3 定义带缓冲的通道

go 复制代码
//ch := make(chan T, N)
// T 通道类型, N 缓冲区长度
//创建一个带缓冲的水箱
waterCh := make(chan string,10)

有缓冲通道的特定:

  • 写者在缓冲未满时不会阻塞(写入缓冲区),只有当缓冲区满了才阻塞;读在缓冲为空时阻塞
  • 可以用来"解耦"生成/消费速率,但缓冲不是无限的

2、通道的基本操作(发送、接收、关闭)

go 复制代码
package main
import (
    "fmt"
    "time"
)
func main() {
    // 无缓冲示例:同步行为
    unbuf := make(chan int)
    go func() {
        fmt.Println("goroutine: 准备发送 1(会等待接收)")
        unbuf <- 1 // 阻塞,直到 main 接收
        fmt.Println("goroutine: 发送完成")
    }()
    time.Sleep(100 * time.Millisecond)
    fmt.Println("main: 开始接收")
    fmt.Println("接收到:", <-unbuf)
    // 有缓冲示例:解耦
    buf := make(chan int, 2)
    buf <- 10 // 不阻塞(缓冲未满)
    buf <- 20 // 仍不阻塞
    // buf <- 30 // 如果再写,会阻塞直到有人读
    fmt.Println("从缓冲接收:", <-buf, <-buf)
    // 关闭 channel(只能由发送方关闭)
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    close(ch)           // 之后不能再发送,会 panic
    for v := range ch { // range 会在 channel 被 close 且数据读尽后结束
        fmt.Println("range got:", v)
    }
    time.Sleep(time.Second * 5)
}

注意:

  • close(ch)的意思是"再也没有新的值会被发送到这个通道"
  • 只有发送方应该关闭通道,接收方关闭会引发panic
  • 向已经关闭的通道发送会导致panic;从已关闭通道接收会立即得到0值 使用comma-ok判断通道是否已关闭:
go 复制代码
v, ok := <-ch
if !ok {
  // ch 已经关闭且已经读完了
}

3、通道与同步

无缓冲通道本身就是最简单的同步原语:写者等待读者,读者等待写者->达成同步点,就像握手一样。

使用空结构体struct{}做信号:done:= make(chan struct{}),发送 done <- struct{}{}表示"完成"通知

也可以把sync.WaitGroup与通道混合使用。

例如:用通道做一次性完成通知

go 复制代码
done := make(chan struct{})
go func() {
    // 工作...
    close(done) // 通知完成(常见做法)
}()
<-done // 等待完成

4、select:多路复用与超时

select类似switch,但只针对通道的发送接收:

go 复制代码
select {
case v := <-ch1:
    fmt.Println("从 ch1 收到", v)
case ch2 <- x:
    fmt.Println("向 ch2 发送成功")
case <-time.After(time.Second * 2):
    fmt.Println("超时了")
default:
    fmt.Println("没有准备好的 case,立即执行 default(非阻塞)")
}

用time.After或者time.Timer可以实现超时或限时等待。select是实现非阻塞操作、超时、同时从多个通道接收的关键结构快。

5、常见模式

5.1 流水线:生产者->第一道工序->第二道工序->...->消费者,利用通道串联处理步骤,易于并行化和组合。

以汽车生产流水线(生产车身->安装发动机->安装轮子->下线)为例:

go 复制代码
package main
import (
    "log"
)
// 1. 生产车身
func createCarBody(carModels ...string) <-chan string {
    bodyCh := make(chan string)
    go func() {
        defer close(bodyCh) // 生产完成后关闭传送带
        for _, model := range carModels {
            log.Printf("第一步:制造 %s 车身\n", model)
            bodyCh <- model // 将车身送到车身传送带上
        }
    }()
    return bodyCh
}
// 2. 安装发动机
func installEngine(bodyCh <-chan string) <-chan string {
    withEnginCh := make(chan string)
    go func() {
        defer close(withEnginCh)
        for car := range bodyCh { // 从车身传送带接收车身
            log.Printf("第二步:为 %s 安装发动机\n", car)
            withEnginCh <- car // 送往下一个传送带
        }
    }()
    return withEnginCh
}
// 3. 安装轮胎
func installWheels(withEnginCh <-chan string) <-chan string {
    withWheelsCh := make(chan string)
    go func() {
        defer close(withWheelsCh)
        for car := range withEnginCh {
            log.Printf("第三步:为 %s 安装轮胎\n", car)
            withWheelsCh <- car
        }
    }()
    return withWheelsCh
}
func main() {
    // 设置生产线
    bodyCh := createCarBody("SUV", "轿车", "跑车", "卡车")
    withEngineCh := installEngine(bodyCh)
    withWheelsCh := installWheels(withEngineCh)
    // 最终质检并出厂
    for car := range withWheelsCh {
        log.Printf("第四步:完成生产,%s 已出厂\n", car)
    }
}

5.2 扇出/扇入:多个worker从同一个jobs通道拉任务(扇出),他们把结果写到results通道(扇入)。

未来提高安装轮胎的效率,增加一个安装轮胎的工位:

go 复制代码
// 合并多个通道的汽车(Fan-In)
func merge(cs ...<-chan string) <-chan string {
    // 创建一个wg等待组
    var wg sync.WaitGroup
    out := make(chan string)
    // 从每个输入通道收集数据
    collect := func(in <-chan string) {
        //协程函数完成,即wg计数减一
        defer wg.Done()
        for car := range in {
            out <- car
        }
    }
    // wg.Add(len(cs))
    for _, c := range cs {
        //wg计数加一
        wg.Add(1)
        go collect(c)
    }
    // 所有输入完成后关闭输出通道
    go func() {
        //等待所有的协程都执行完了才放过
        wg.Wait()
        close(out)
    }()
    return out
}
func main() {
    carBodies := createCarBody("SUV", "轿车", "跑车", "卡车")
    withEngines := installEngine(carBodies)
    // Fan-Out: 创建2个轮胎安装工位并行工作
    withWheelsCh1 := installWheels(withEngines) // 工位1
    withWheelsCh2 := installWheels(withEngines) // 工位2
    // Fan-In: 合并两个工位的输出
    finishedCars := merge(withWheelsCh1, withWheelsCh2)
    for car := range finishedCars {
        log.Printf("第四步:完成生产, %s\n", car)
    }
}

优雅退出:

实际生产中可能需要处理中断,例如紧急停机:

go 复制代码
func installEngine(done <-chan struct{}, in <-chan string) <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        for car := range in {
            select {
            case out <- car:
                fmt.Printf("为 %s 安装发动机\n", car)
            case <-done: // 收到停机信号
                fmt.Println("发动机安装工位紧急停机")
                return
            }
        }
    }()
    return out
}

5.3 工作池:

模拟3个工人要完成5条任务

go 复制代码
package main

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

func main() {
    // 1. 创建两个通道
    jobs := make(chan int, 5) // 任务队列,最多放5个任务
    done := make(chan bool)    // 用于通知所有任务完成的信号

    var wg sync.WaitGroup // 用于等待所有工作者完成

    // 2. 启动3个工作者 (Worker)
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, &wg)
    }

    // 3. 向任务队列添加5个任务
    for j := 1; j <= 5; j++ {
        jobs <- j // 发送任务编号
    }
    close(jobs) // 重要:发送完所有任务后关闭通道

    // 4. 等待所有工作者完成任务
    go func() {
        wg.Wait()   // 阻塞直到所有工作者都结束
        close(done) // 发送完成信号
    }()

    // 5. 主程序在此等待,直到收到完成信号
    <-done
    fmt.Println("所有任务完成!")
}

// 工作者函数:从jobs通道接收任务并处理
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
    defer wg.Done() // 函数退出时通知WaitGroup此工作者已完成

    for j := range jobs { // 循环从jobs通道读取任务,直到通道关闭
        fmt.Printf("工作者%d 开始处理任务%d\n", id, j)
        time.Sleep(time.Second) // 模拟任务耗时
        fmt.Printf("工作者%d 完成处理任务%d\n", id, j)
    }
}

通道在协程里的作用,就像人与人之间的沟通:

有时候我们需要面对面沟通(无缓冲通道);有时候需要有空的时候才回微信消息(有缓冲通道);select语句就像你同时处理多条消息,决定先回复哪一条或者不回复。学会恰当地选用同步或者缓冲,设置超时和优雅退出,代码会更健壮,人与人之间也是如此:及时、明确、还有体贴,是减少误解的关键。

相关推荐
Coding君1 小时前
每日一Go-6、Go语言结构体(Struct)与面向对象的实现方式
go
Coding君1 小时前
每日一Go-8、Go语言错误处理机制
go
Coding君1 小时前
每日一Go-7、Go语言接口(Interface)
go
喵个咪1 小时前
微服务技术选型:从生态架构视角看go-kratos的不可替代性
后端·go
avilang19 小时前
如何在 Go 1.24+ 中管理 tool 依赖
go
程序员爱钓鱼19 小时前
用 Go 做浏览器自动化?chromedp 带你飞!
后端·go·trae
小信啊啊1 天前
Go语言结构体
golang·go
moxiaoran57532 天前
Go语言的常量
go
武大打工仔2 天前
如何理解 Golang 中的 Context?
go