channel

文章目录

  • channel
    • 简介
    • 基本概念
      • 类型表示法
      • 值表示法
      • 操作的特性
      • 初始化通道
      • 接收元素值
      • Happens before
      • 发送值
        • 例1
          • 核心组件
          • 关键执行顺序
          • 输出示例(可能顺序)
          • 设计要点
        • 例2
        • 例3
      • 关闭通道
      • 长度与容量
    • 单向通道
      • 主要用途
        • 增强代码表达性和安全性(最重要的用途)
        • 实现更严格的接口
      • 使用示例
        • 示例 1:经典的生产者-消费者模型
        • 示例 2:函数返回一个只读通道
      • 重要注意事项
      • 总结
    • 配合 `for` 语句与 `select` 语句
      • 核心思想
      • `for range` (最常用、最推荐)
      • 显式检查 (较少用)
        • 基础语法
      • 核心应用模式
        • 模式 1:多路复用 (Multiplexing)
        • 模式 2:超时控制 (Timeout)
        • 模式 3:非阻塞操作 (Non-blocking)
      • For 配合 Select 一起使用
        • 经典结构:带退出机制的永久循环
        • 更现代的结构:使用 `context.Context`
      • 总结与最佳实践表格
      • 黄金法则
    • 配合 `time` 包使用
      • 基本用法
        • 1. 定时器 (Timer)
        • 2. 打点器 (Ticker)
      • 实际应用场景
        • 1. 超时控制
        • 2. 定期执行任务
        • 3. 限制操作频率
        • 4. 带有超时的等待组
      • 注意事项
        • 4. 带有超时的等待组
      • 注意事项

简介

这里会详细介绍 Go 的并发编程理念:

以通信作为手段来共享内存, 而不是通过共享内存来通信

这一理念的最直接最重要的体现也就是 channel.

Go 鼓励用与众不同的方法来共享值, 这个方法就是用一个通道(信道)类型在不同 goroutine 之间传递值. Go 的 channel 就像是一个类型安全的通用型管道.

channel 提供一种机制, 使得即可以同步两个并发执行函数, 又可以让两个函数通过相互传递特定类型值来通信. 也就是说提供了两个功能:

  • 并发 goroutine 同步
  • 并发 goroutine 通信

当然有些场景下, 使用共享变量和传统同步方法更加方便, 但是作为高级用法, 使用 channel 可以让我们编写更加清晰正确的程序.

基本概念

Go 中, channel 既指通道类型, 也指代可以传递某种类型的值的通道.

通道即某一个通道类型的值, 是该类型的一个实例.

类型表示法

通道是一个引用类型, 这和切片以及字典这两个类型是一致的. 一个泛化的通道类型声明应当如此:

chan T

其中,

  • 关键字 chan 是代表通道类型的关键字,
  • T 是表示了该通道类型的元素类型. 限制了可以经由此类通道传递的元素值的类型.

可以声明这样一个别名类型:

type IntChan chan int

该别名类型代表了元素类型为 int 的通道类型.

又比如可以直接声明一个 chan int 类型的变量:

var intChan chan int

初始化之后, intChan 变量就可用来传递 int 类型的值.

以上就是最简单的通道类型生命方式, 这样声明的通道类型是双向的. 也就是既可以向它发送值, 也可从他接收值.

此外还可声明单向的通道类型, 需要用到接收操作符 <-, 下面就是一个只能用于发送值的通道类型的泛化表示:

chan<- T

只能向此类通道发送值而不能从其中接收值. 接受操作符 <- 生动表示了元素值的流向. 可以把这样的单项通道类型简称为发送通道类型. 同样的也可以声明只能从中接收值的通道类型:

<-chan T

这类单向通道类型可以被简称为: 接收通道类型.

值表示法

因为 channel 是引用类型, 所以通道类型的变量在被初始化之前, 其值一定是 nil.

注意: 通道的语义决定了他和其他类型不同, 通道类型的变量一定是用来传递值的, 而不是用来存储值的, 所以通道类型没有对应的值表示法. 其值有即时性, 无法用字面量来准确表达.

操作的特性

通道是在多个 goroutine 间传递数据和同步的重要手段, 对于通道的操作, 其本身也是同步的.

在同一时刻, 只能有一个 goroutine 向一个通道发送值, 同时也只能有一个 goroutine 从它那接受值.

通道相当于一个 FIFO 的消息队列, 其中的各个值都是严格按照发送到其中的先后顺序排列, 最早被发送到通道的值会最先被接收.

通道中的值都有原子性, 不可以被分割.

通道中的每一个值都只能被某一个 goroutine 接收, 已经被接受的值会立刻从通道中删除.

初始化通道

所有引用类型的值都要使用 make 内建函数初始化, channel 也是这样的:

make(chan int, 10)

将初始化一个最多能缓冲 10 个 int 类型值的 channel (这是一个带有缓冲区的 channel), 一个带有缓冲的 channel, 其缓冲容量总是固定不变的.

此处也可以省略第二个参数, 此时创建的就是一个无缓冲区的 channel:

make(chan int)

发送给该 channel 的值应当被立刻取走, 否则发送方的 goroutine 将会阻塞, 直到有接收方接受了该值.

接收元素值

接收运算符 <- 既可以用来作为通道类型声明的一部分, 也可用于通道操作(发送或者接收元素值).

假设有这样的一个通道类型的变量:

strChan := make(chan string, 3)

make 函数调用后, 返回一个已经被初始化的通道值作为结果.

因此该赋值语句时的变量 strChan 成为一个双向通道, 该通道的元素类型为 string, 容量为 3.

如果要从中接收元素值, 那么这样写:

elem := <-strChan

语义很简单: 将 strChan 中的一个值赋值给变量 elem.

该操作将使当前 goroutine 被迫进入 Gwaiting 状态, 直到 strChan 之中有新的值可取时才会被唤醒.

也可以用以下的双返回值:

elem, ok := <-strChan

这里同样是一个阻塞行为.

如果在进行接收操作之前或者过程中该通道被关闭了, 那么该操作将会立即结束, 变量 elem 会被赋予该通道的元素类型的零值(0, nil等). 这对应着一个特殊情况, 如果我们接收到的值本身就是零值而不是由于关闭通道而产生的异常零值要怎么办, 此时 ok 就有作用了.

ok 是一个 bool 类型变量, 当接收操作因通道关闭而结束时, 该值为 false, 否则就是 true.

可以把在符号 = 或者 := 右侧出现的, 仅能是接收表达式的赋值语句称为接收语句.

在其中的接收操作符 <- 右边的不仅仅可以是代表通道的标识符, 也可是任意的表达式.

只要该表达式的结果类型是一个通道类型即可, 将这样的表达式称作通道表达式.

最后要注意: 尝试从一个未被初始化的通道值(也就是一个值为 nil 的通道)接收值, 会造成当前 goroutine 的永久阻塞.

Happens before

为了能够从通道接收元素值,我们先向它发送元素值.

理所当然,一个元素值在被接收方从通道中取出之前,必须先存在于该通道内.

更加正式地讲,对于一个缓冲通道,有如下规则:

  • 发送操作会使通道复制被发送的元素.

    如果因通道的缓冲空间已满而无法立即复制,则阻塞进行发送操作的 goroutine.

    复制的目的地址有两种:

    • 当通道已空且有接收方在等待元素值时,它会是最早等待的那个接收方持有的内存地址(channel就像是中转了一下)
    • 否则是通道持有的缓冲中的内存地址。
  • 接收操作会使通道给出一个已发给它的元素值的副本,若因通道的缓冲空间已空而无法立即给出,则阻塞进行接收操作的goroutine。一般情况下, 接收方会从通道持有的缓冲中得到元素值。

  • 对于同一个元素值来说,把它 发送给某个通道 的操作,总是会在 从该通道接收它 这一操作之前完成。

    换句话说,在通道完全复制一个元素值之前,任何 goroutine 都不可能从它那里接收到这个元素值的副本。

发送值

发送语句由三要素组成:

  • 通道表达式
  • 接收操作符 <-
  • 代表元素值的表达式(以下简称为元素表达式)

其中, 元素表达式 的结果类型一定要和 通道表达式 的结果类型中的元素类型之间存在可赋值关系. 也就是, 前者的值一定是可以赋给类型为后者的变量.

对于接收表达式 <- 两边的表达式的求值总是先于发送操作执行, 对两个表达式的求值完成前, 发送操作一定会被阻塞.

比如想要向通道 strChan 发送一个值 "a", 要这样做:

先初始化一个通道,

strChan := make(chan string, 3)

然后:

strChan <- "a"

<- 左侧是将要接纳元素值的通道, 右边则是想要发送给该通道的值.

此表达式被求值后, 通道 strChan 就缓冲了值 “a”, 然后再往里边发两个值:

strChan <- "b"
strChan <- "c"

现在 通道 strChan 缓冲了3个元素值, 达到了最大容量. 此后某个 goroutine 再向其中发送元素值时, 该 goroutine 就会被阻塞, 只有从该通道中接受一个元素值后, 这个 goroutine 才会被唤醒并且完成发送操作.

例1

看一段代码:

package mainimport ("fmt""time"
)var strChan = make(chan string, 3)func main() {syncChan1 := make(chan struct{}, 1)syncChan2 := make(chan struct{}, 2)// 用于演示接收操作go func() {<-syncChan1 // 等待同步信号(阻塞直到收到 "c")fmt.Println("Received a sync signal and wait a second ... [receiver]")time.Sleep(time.Second)  // 故意等待 1 秒// 循环接收数据直到通道关闭for {if elem, ok := <-strChan; ok {fmt.Println("Received:", elem, "[receiver]")} else {break // 通道关闭时退出}}fmt.Println("Stopped. [receiver]")syncChan2 <- struct{}{} // 发送完成信号}()// 用于演示发送操作go func() {// 发送数据 "a", "b", "c", "d"for _, elem := range []string{"a", "b", "c", "d"} {strChan <- elemfmt.Println("Sent:", elem, "[sender]")// 关键同步点:发送 "c" 后触发接收者启动if elem == "c" {syncChan1 <- struct{}{} // 发送同步信号fmt.Println("Sent a sync signal. [sender]")}}// 完成发送后等待 2 秒fmt.Println("Wait 2 seconds... [sender]")time.Sleep(time.Second * 2)close(strChan)      // 关闭数据通道syncChan2 <- struct{}{} // 发送完成信号}()<-syncChan2<-syncChan2
}

这段 Go 代码演示 goroutine 间的同步与通信,使用了带缓冲的 channel 和同步信号 channel.

核心组件
  1. strChan:缓冲大小为 3 的字符串 channel,用于数据传输
  2. syncChan1:缓冲大小为 1 的同步信号 channel(控制接收启动时机)
  3. syncChan2:缓冲大小为 2 的同步信号 channel(等待两个 goroutine 结束)
关键执行顺序
  1. 初始发送阶段

    • 发送者快速发送 “a”, “b”, “c”(填满 3 缓冲)
    • 发送 “c” 后触发 syncChan1 信号
    • 发送 “d” 时阻塞(因缓冲已满)
  2. 接收启动阶段

    • 接收者收到 syncChan1 信号
    • 等待 1 秒后开始消费数据
    • 接收 “a” 后释放缓冲空间
  3. 完成阶段

    • 发送者解除阻塞,发送 “d”
    • 发送者等待 2 秒后关闭 strChan
    • 接收者消费剩余数据 (“b”, “c”, “d”) 后退出
输出示例(可能顺序)
Sent: a [sender]
Sent: b [sender]
Sent: c [sender]
Sent a sync signal. [sender]  // 发送者在此阻塞
Received a sync signal... [receiver]
// (1秒延迟)
Received: a [receiver]       // 释放缓冲
Sent: d [sender]             // 发送者解除阻塞
Wait 2 seconds... [sender]   // 发送者开始等待
Received: b [receiver]
Received: c [receiver]
Received: d [receiver]       // 接收者消费完毕
Stopped. [receiver]          // 接收者退出
// (主 goroutine 收到两个完成信号后退出)
设计要点
  1. 缓冲控制:缓冲大小为 3 使得发送 “d” 时被阻塞
  2. 精确同步syncChan1 确保接收者在特定时点启动(收到 “c” 后)
  3. 关闭通道close(strChan) 通知接收者数据结束
  4. 双信号确认syncChan2 保证主 goroutine 等待所有任务完成

此代码演示了如何通过 channel 实现:

  • 数据传输 (strChan)
  • 启动时机控制 (syncChan1)
  • 任务完成同步 (syncChan2)
  • 通道关闭通知机制
例2

对于通道的复制行为还需要再解释解释. 发送方 向 通道发送的值会被复制, 接收方接受的总是该值的副本而不是该值的本身,

这意味着对于一个值对象来说, 就是做了普通的一次拷贝而已,

但是对于引用类型来说(比如切片, 字典等), 就是复制了一份引用, 这也意味着修改了复制之后得到的引用就修改了收发两方持有的值.

package mainimport ("fmt""time"
)var mapChan = make(chan map[string]int, 1)func main() {syncChan := make(chan struct{}, 2)// 发go func() {countMap := make(map[string]int)for i := 0; i < 5; i++ {mapChan <- countMaptime.Sleep(time.Millisecond)fmt.Printf("The count map: %v. [sender]\n", countMap)}close(mapChan)syncChan <- struct{}{}}()// 收go func() {for {if elem, ok := <-mapChan; ok {elem["count"]++} else {break}}fmt.Println("Stopped. [receiver]")syncChan <- struct{}{}}()<-syncChan<-syncChan
}

输出如下:

The count map: map[count:{count: 1}]. [sender]
The count map: map[count:{count: 2}]. [sender]
The count map: map[count:{count: 3}]. [sender]
The count map: map[count:{count: 4}]. [sender]
The count map: map[count:{count: 5}]. [sender]
Stopped. [receiver]

这里收的一方就对得到的值做了自增操作, 这里也能看到, 原值同样被更改了.

例3
package mainimport ("fmt""time"
)type Counter struct {count int
}var mapChan = make(chan map[string]Counter, 1)func main() {syncChan := make(chan struct{}, 2)// 发go func() {countMap := map[string]Counter{"count": {},}for i := 0; i < 5; i++ {mapChan <- countMaptime.Sleep(time.Millisecond)fmt.Printf("The count map: %v. [sender]\n", countMap)}close(mapChan)syncChan <- struct{}{}}()// 收go func() {for {if elem, ok := <-mapChan; ok {counter := elem["count"]counter.count++} else {break}}fmt.Println("Stopped. [receiver]")syncChan <- struct{}{}}()<-syncChan<-syncChan
}

这里将输出:

The count map: map[count:{0}]. [sender]
The count map: map[count:{0}]. [sender]
The count map: map[count:{0}]. [sender]
The count map: map[count:{0}]. [sender]
The count map: map[count:{0}]. [sender]
Stopped. [receiver]

原因是 go 中的结构体都是值类型而非引用类型, 如果要修改原值的话就要传入指针

比如将变量 mapChancountMap 修改为:

var mapChan = make(chan map[string]*Counter, 1)
countMap := map[string]*Counter{"count": {},
}

为了观察结构体内部的值状态及其变化, 为 Counter 类型增加一个方法:

func (counter *Counter) String() string {return fmt.Sprintf("{count: %d}", counter.count)
}

然后输出将是如此:

The count map: map[count:{count: 1}]. [sender]
The count map: map[count:{count: 2}]. [sender]
The count map: map[count:{count: 3}]. [sender]
The count map: map[count:{count: 4}]. [sender]
The count map: map[count:{count: 5}]. [sender]
Stopped. [receiver]

关闭通道

调用 close 函数可以关闭一个通道, 但是调用前要注意: 如果向已关闭的通道发送元素, 此操作会引发 panic, 所以在关闭通道之前应当确保安全(后面会说用 for 语句和 select 语句确保安全.

这里要说明: 无论如何都不能在接收端关闭通道, 因为接收端在逻辑上一般是不发判断发送端是否还会向通道发元素值. 另一个方面来说, 从发送端关闭通道一般不会产生什么影响, 就算是关闭之后通道里面还有值, 也是可以通过接收表达式取出的, 然后根据该表达式第二个结果值判断通道是否已关闭并且没有元素值可以取.

然后看一个示例:

package mainimport "fmt"func main() {dataChan := make(chan int, 5)syncChan1 := make(chan struct{}, 1)syncChan2 := make(chan struct{}, 2)// 发go func() {for i := 0; i < 5; i++ {dataChan <- ifmt.Printf("[sender] Sent: %d\n", i)}close(dataChan)syncChan1 <- struct{}{}fmt.Println("[sender] Done.")syncChan2 <- struct{}{}}()// 收go func() {<-syncChan1for {if elem, ok := <-dataChan; ok {fmt.Printf("[receiver] Received: %d \n", elem)} else {break}}fmt.Println("[receiver] Done.")syncChan2 <- struct{}{}}()<-syncChan2<-syncChan2
}

这里通过一个通道(dataChan1)阻塞了收方面, 强制发送完所有元素并且关闭通道之后再执行接收操作. 虽然通道在这里已关闭了, 但是对于接受操作却没有影响, 接收方仍然可以再接受完所有元素值之后结束工作.

最后又两个注意点:

  • 同一通道只能关闭一次, 关闭一个已关闭的通道会引发 panic
  • 调用 close 函数时, 需要把想要关闭的通道的变量作为参数传入, 如果该变量值为 nil, 将引发 panic.

为了帮助决策,可以遵循以下原则:

场景是否需要关闭?说明
你是发送方,且不再发送任何值接收方正使用 for range必须关闭这是关闭 channel 最主要的原因。
你是发送方,需要通知多个接收者“结束了”应该关闭关闭 channel 是一种广播机制,所有接收的 for range 都会收到。
Channel 用于单向同步信号(如 done <- struct{}{}不需要关闭接收方只接收一次,不关心后续状态。
Channel 是全局的、永久的(如任务队列)不需要关闭它的设计就是永不停止。但消费者逻辑要匹配(用 for-select 而非 for range)。
你不确定倾向于关闭除非你有明确理由不关闭,否则关闭一个 channel 通常比不关闭更安全。但切记:只能关闭一次,且不能关闭已关闭的 channel

记住最后的黄金法则永远不要关闭一个接收方还在等待读取的 channel,并且只能由发送方来关闭(或者一个非常明确知道没有其他发送者的角色)。关闭一个 channel 的意图应该是向接收方发送信息,而不是为了回收资源。

长度与容量

内建函数 lencap 同样可以用在通道上, 作用分别是获取通道中当前元素值的数量(长度)以及获取通道可容纳元素值的最大数量(容量). 通道容量实在初始化时以确定的, 并且之后不能改变, 通道长度则会随实际情况变化.

可以通过容量判断通道是否带缓冲. 如果容量为 0, 那么一定是非缓冲通道, 否则是缓冲通道.

单向通道

这是一个非常重要的概念,主要用于在函数或方法间传递通道时,施加明确的权限限制,从而增强代码的类型安全性和可读性。

顾名思义,单向通道就是只能用于发送或只能用于接收的通道。它是双向通道的一种变体,其类型由 chan<-(只写)和 <-chan(只读)表示。

  • 只写通道chan<- T
    • 你只能向这个通道发送数据(ch <- value)。
    • 你不能从这个通道接收数据(如果尝试 <-ch 会引发编译错误)。
  • 只读通道<-chan T
    • 你只能从这个通道接收数据(value := <-ch)。
    • 你不能向这个通道发送数据(如果尝试 ch <- value 会引发编译错误)。

一个普通的双向通道 chan T 可以被隐式转换为任何一种单向通道,但反过来不行。


主要用途

你可能会问,既然有双向通道,为什么要限制自己呢?其主要目的是为了在接口层面强制约定和保证代码安全

增强代码表达性和安全性(最重要的用途)

当一个函数或方法的参数是一个通道时,使用单向通道可以清晰地表达这个函数的意图

  • 对于函数参数:它明确规定了函数对这个通道的操作权限
    • func producer(ch chan<- int): 我(producer 函数)承诺只会在 ch 里写数据,绝不会尝试读。这相当于一个“合同”。
    • func consumer(ch <-chan int): 我(consumer 函数)承诺只会从 ch 里读数据,绝不会尝试写。

这样做的好处是:

  1. 自文档化:任何人看到函数签名,立刻就知道这个通道该怎么用。
  2. 编译时检查:编译器会帮你抓住所有违反这个约定的操作。如果你不小心在 consumer 函数里写了 ch <- data,代码将无法通过编译。这是一种强大的、在编译阶段就能发现错误的机制。
  3. 防止误操作:避免在复杂的并发程序中,错误地关闭了不该关闭的通道,或者向一个本应只读的通道发送数据,导致难以调试的 panic。
实现更严格的接口

在设计库或者模块时,你可以暴露只读或只写通道给外部使用者,从而隐藏内部实现细节,防止外部代码错误地干扰你的内部通信逻辑。


使用示例

示例 1:经典的生产者-消费者模型

这是最典型的使用场景。

package mainimport ("fmt""time"
)// 生产者函数:接收一个只写通道
// 它只能向这个通道发送数据
func producer(ch chan<- int) {for i := 0; i < 5; i++ {fmt.Printf("生产者发送: %d\n", i)ch <- i // 这是允许的time.Sleep(time.Second)}close(ch) // 关闭通道也是允许的(通常由发送方关闭)// 注意:从一个只写通道接收数据(如 <-ch)会导致编译错误
}// 消费者函数:接收一个只读通道
// 它只能从这个通道接收数据
func consumer(ch <-chan int) {// 循环从通道中读取数据,直到通道被关闭for num := range ch {fmt.Printf("消费者收到: %d\n", num)}// 向一个只读通道发送数据(如 ch <- 99)会导致编译错误// 关闭一个只读通道(如 close(ch))也会导致编译错误
}func main() {// 1. 创建一个普通的双向通道ch := make(chan int)// 2. 启动生产者和消费者goroutine// 在传参时,Go语言会自动将双向通道 ch 转换为所需的单向通道类型go producer(ch) // ch 被当作 chan<- int 使用consumer(ch)    // ch 被当作 <-chan int 使用fmt.Println("程序结束")
}

关键点

  • main 函数里创建的是双向通道 chan int
  • 在将 ch 传递给 producer 时,它被隐式转换为了 chan<- int(只写)。
  • 在将 ch 传递给 consumer 时,它被隐式转换为了 <-chan int(只读)。
  • 这种转换是安全的,并且是 Go 语言类型系统所允许的。
示例 2:函数返回一个只读通道

你可以设计一个函数,它返回一个只读通道,调用者只能从这个通道消费数据,无法向其发送数据,这很好地封装了内部逻辑。

// 创建一个计数器,返回一个只读通道,每秒发送一个递增的数字
func startCounter() <-chan int {ch := make(chan int)go func() {defer close(ch)for i := 0; ; i++ {ch <- itime.Sleep(time.Second)}}()return ch // 返回的 chan int 被隐式转换为 <-chan int
}func main() {countCh := startCounter()// 我们只能从 countCh 读for i := 0; i < 3; i++ {fmt.Println(<-countCh)}// countCh <- 100 // 错误:不能向只读通道发送// close(countCh) // 错误:不能关闭只读通道
}

重要注意事项

  1. 转换是单向的:你可以将 chan T 转换为 chan<- T<-chan T,但不能将 chan<- T<-chan T 转换回 chan T。这是一条“单行道”,目的是为了保证安全。
  2. 通道操作权限
    • 关闭操作:只有发送方可以关闭通道。因此,你可以在一个 chan<- T 上调用 close(),但不能在一个 <-chan T 上调用 close(),这会导致编译错误。
    • 长度和容量:你可以使用 len()cap() 来查询只读和只写通道,因为这个操作不涉及数据的发送和接收。

总结

通道类型操作权限典型用途
chan T双向(可读可写)在单个 goroutine 内部或多个 goroutine 间自由通信
chan<- T只写(发送)作为函数参数,限制函数只能向通道发送数据
<-chan T只读(接收)作为函数参数或返回值,限制函数只能从通道接收数据

最佳实践:在函数或方法的签名中,尽可能地使用单向通道。这是一种“按权限设计”的思路,它能极大地提高并发代码的清晰度、安全性和可维护性,是编写高质量 Go 并发程序的标志之一。

配合 for 语句与 select 语句

好的,这是一份为你整理的关于 Go 语言中 channelforselect 语句配合使用的综合笔记。它涵盖了核心概念、各种模式、最佳实践和注意事项,非常适合用于学习和复习。

核心思想

Channel 是 Goroutine 之间的通信管道,而 forselect 是消费和管理这些管道的主要控制流语句。它们的组合构成了 Go 并发编程的基石。

  • for:用于持续地从 channel 中接收数据。
  • select:用于同时监听多个 channel 的操作(发送或接收)。
  • for + select:用于构建长期运行的服务,该服务需要多路处理各种事件(如数据、信号、超时)。

for range (最常用、最推荐)

行为:自动从 channel 接收值,直到 channel 被关闭且 drained(排空)。

循环结束条件:channel 被关闭。

关键发送方负责关闭 channel,以向接收方广播“没有更多数据”的信号。

ch := make(chan int)// 生产者 Goroutine
go func() {for i := 0; i < 3; i++ {ch <- i}close(ch) // 重要!由发送方关闭
}()// 消费者:使用 for range
for value := range ch {fmt.Println(value) // 打印 0, 1, 2
}
// 循环在 ch 关闭后自动退出
fmt.Println("Channel closed, loop exited.")

最佳实践:在简单消费者场景下优先使用此模式。


显式检查 (较少用)

行为:使用 , ok 语法手动检查 channel 状态。

循环结束条件ok == false(channel 已关闭且空)。

for {value, ok := <-chif !ok {break // channel 已关闭且空,退出循环}fmt.Println(value)
}

select 用于监听多个 channel 操作,每个 case 是一个通信操作。

跟在每个 case 后面的之呢个是针对某个通道的发送语句或者接收语句。

select 关键字右侧没有像是 switch 语句那样的 switch 表达式, 而是直接跟上左花括号。

基础语法
select {
case v := <-chan1:fmt.Printf("Received %v from chan1\n", v)
case chan2 <- data:fmt.Println("Sent data to chan2")
case <-chan3:fmt.Println("Received something from chan3 (value ignored)")
default:fmt.Println("No communication ready, do something else")
}

开始执行 select 语句时, 所有在 case 右侧的发送语句或者接收语句中的通道表达式和元素表达式都会先求值(求值顺序是从左到右, 自上而下), 无论他们所在的 case 是否可能被选择都是这样.

在执行 select 语句的时候,运行时系统会自上而下地判断每个 case 中的发送或接收操作是否可以立即进行。

这里的“立即进行”,指的是当前 goroutine 不会因此操作而被阻塞。

这个判断还需要依据通道的具体特性(缓冲或非缓冲)以及那一时刻的具体情况来进行。

只要发现有一个 case 上的判断是肯定的,该 case 就会被选中。

package mainimport "fmt"var intChan1 chan int
var intChan2 chan int
var channels = []chan int{intChan1, intChan2}var numbers = []int{1, 2, 3, 4, 5}func main() {select {case getChan(0) <- getNumber(0):fmt.Println("1th case is selected.")case getChan(1) <- getNumber(1):fmt.Println("The 2nd case is selected.")default:fmt.Println("Default case!")}
}func getNumber(i int) int {fmt.Printf("numbers[%d]\n", i)return numbers[i]
}func getChan(i int) chan int {fmt.Printf("channels[%d]\n", i)return channels[i]
}

输出:

channels[0]

numbers[0]

channels[1]

numbers[1]

Default case!

select 会阻塞直到某个 case 就绪,并在多个 case 就绪时伪随机公平地选择一个执行。

如果没有任何一个 case 符合选择条件, 而且没有 default case, 那么当前 goroutine 将保持阻塞, 直到至少有一个 case 中的发送或者接受操作可以立即进行为止.


核心应用模式

模式 1:多路复用 (Multiplexing)

监听多个 channel,处理最先到达的事件。

dataChan := make(chan string)
stopChan := make(chan struct{}) // 用于信号的 channelfor {select {case data := <-dataChan:handleData(data)case <-stopChan:// 收到停止信号,清理并退出fmt.Println("Stopping...")return}
}
模式 2:超时控制 (Timeout)

防止 Goroutine 无限期阻塞。使用 time.Aftercontext

select {
case result := <-longRunningOperationChan:fmt.Println("Success:", result)
case <-time.After(2 * time.Second):fmt.Println("Error: Operation timed out after 2 seconds")
}

注意:在长周期循环中使用 time.After 会创建大量 Timer,可能导致资源泄漏。应使用 time.NewTimer 并在循环外创建和重置。

timer := time.NewTimer(2 * time.Second)
defer timer.Stop() // 确保释放资源for {timer.Reset(2 * time.Second) // 每次循环重置select {case result := <-operationChan:handle(result)case <-timer.C:handleTimeout()}
}
模式 3:非阻塞操作 (Non-blocking)

使用 default 分支尝试立即进行通信,若无法完成则执行其他任务。

select {
case ch <- task: // 尝试发送fmt.Println("Task sent")
default:fmt.Println("Channel is busy, skipping task or adding to a buffer")// 例如,实现一个简单的负载下降策略
}

For 配合 Select 一起使用

这是构建复杂并发服务(如 worker pools、网络服务器、事件循环)的核心模式

经典结构:带退出机制的永久循环
func worker(inputChan <-chan *Task, stopChan <-chan struct{}) {for { // 永久循环select {case task := <-inputChan: // 1. 处理主要工作process(task)case <-stopChan: // 2. 响应退出信号fmt.Println("Worker shutting down...")cleanup()return // 退出函数,从而结束 Goroutinecase <-time.After(30 * time.Second): // 3. 处理超时/空闲状态fmt.Println("Worker is idle")}}
}
更现代的结构:使用 context.Context

context 包提供了更强大、更标准的取消和超时机制。

func worker(ctx context.Context, inputChan <-chan *Task) {for {select {case task := <-inputChan:process(task)case <-ctx.Done(): // 监听 Context 的取消/超时信号err := ctx.Err()fmt.Printf("Worker stopping due to: %v\n", err)cleanup()return}}
}// 在主函数中
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() // 确保资源释放go worker(ctx, taskChan)

总结与最佳实践表格

模式语法用途结束条件
for range chfor v := range ch { }简单消费者,处理所有数据ch 被关闭
for + selectfor { select { case ... } }复杂消费者,多路事件处理returnbreak (通常由信号触发)
select + time.Aftercase <-time.After(d)单次操作超时控制超时或操作完成
select + defaultdefault: ...非阻塞通信尝试立即执行

黄金法则

  1. 关闭原则永远只由发送方关闭 channel。关闭一个已关闭的 channel 会引发 panic。
  2. 循环退出for range 依赖 channel 关闭来退出。for-select 循环通常依赖一个专门的信号 channel(如 stopChanctx.Done())来触发退出。
  3. nil Channel:对一个 nil channel 的操作会永远阻塞。你可以利用这一点在 select 中动态“禁用”某个 case(将其设置为 nil)。
  4. 资源管理
    • 使用 defer 关闭 channel(如果是发送方)。
    • 避免在长循环中频繁使用 time.After(),改用 time.Timer
    • 使用 context.Context 来传播取消信号,这是处理超时和取消的现代标准方式。
  5. 预防泄漏:确保 Goroutine 总有办法退出(通过 channel 关闭或信号),否则会导致 Goroutine 泄漏。

配合 time 包使用

在 Go 语言中,time 包与 channel 的配合使用非常强大,主要用于实现超时控制、定时任务和周期性操作等场景。

基本用法

1. 定时器 (Timer)

定时器用于在未来的某个时间点执行一次操作。

package mainimport ("fmt""time"
)func main() {// 创建一个 2 秒的定时器timer := time.NewTimer(2 * time.Second)// 等待定时器触发<-timer.Cfmt.Println("定时器触发")// 停止定时器(如果还需要使用,可以使用 Reset)// timer.Stop()
}
2. 打点器 (Ticker)

打点器用于每隔一段时间重复执行操作。

func main() {// 创建一个每秒触发一次的打点器ticker := time.NewTicker(1 * time.Second)// 创建一个 5 秒后触发的定时器用于停止打点器stopTimer := time.NewTimer(5 * time.Second)for {select {case <-ticker.C:fmt.Println("打点器触发")case <-stopTimer.C:fmt.Println("停止打点器")ticker.Stop()return}}
}

实际应用场景

1. 超时控制
func main() {// 创建一个用于模拟长时间操作的 channelresultChan := make(chan string)// 模拟一个耗时操作go func() {time.Sleep(3 * time.Second)resultChan <- "操作完成"}()// 设置超时时间为 2 秒select {case res := <-resultChan:fmt.Println(res)case <-time.After(2 * time.Second):fmt.Println("操作超时")}
}
2. 定期执行任务
func main() {ticker := time.NewTicker(2 * time.Second)done := make(chan bool)go func() {for {select {case <-done:returncase t := <-ticker.C:fmt.Println("定期任务执行于", t.Format("15:04:05"))}}}()// 运行 10 秒后停止time.Sleep(10 * time.Second)ticker.Stop()done <- truefmt.Println("定时任务停止")
}
3. 限制操作频率
func main() {requests := make(chan int, 5)for i := 1; i <= 5; i++ {requests <- i}close(requests)// 限制为每 1 秒处理一个请求limiter := time.Tick(1 * time.Second)for req := range requests {<-limiterfmt.Println("处理请求", req, time.Now().Format("15:04:05"))}
}
4. 带有超时的等待组
func main() {var wg sync.WaitGroupwg.Add(1)done := make(chan bool)go func() {time.Sleep(3 * time.Second) // 模拟耗时任务wg.Done()done <- true}()// 设置 2 秒超时select {case <-done:fmt.Println("任务完成")case <-time.After(2 * time.Second):fmt.Println("任务超时")}
}

注意事项

  1. 记得调用 Stop() 方法来释放定时器/打点器资源,避免内存泄漏
  2. 使用 time.After() 在长时间运行的循环中可能会创建大量定时器,应考虑使用 time.NewTimer() 并重用
  3. 定时器/打点器触发后,channel 会接收到一个时间值,但通常我们只关心触发事件本身

这些模式使得 Go 程序能够优雅地处理时间相关的操作,特别是在并发环境中非常有用。

(“定期任务执行于”, t.Format(“15:04:05”))
}
}
}()

// 运行 10 秒后停止
time.Sleep(10 * time.Second)
ticker.Stop()
done <- true
fmt.Println("定时任务停止")

}


#### 3. 限制操作频率```go
func main() {requests := make(chan int, 5)for i := 1; i <= 5; i++ {requests <- i}close(requests)// 限制为每 1 秒处理一个请求limiter := time.Tick(1 * time.Second)for req := range requests {<-limiterfmt.Println("处理请求", req, time.Now().Format("15:04:05"))}
}
4. 带有超时的等待组
func main() {var wg sync.WaitGroupwg.Add(1)done := make(chan bool)go func() {time.Sleep(3 * time.Second) // 模拟耗时任务wg.Done()done <- true}()// 设置 2 秒超时select {case <-done:fmt.Println("任务完成")case <-time.After(2 * time.Second):fmt.Println("任务超时")}
}

注意事项

  1. 记得调用 Stop() 方法来释放定时器/打点器资源,避免内存泄漏
  2. 使用 time.After() 在长时间运行的循环中可能会创建大量定时器,应考虑使用 time.NewTimer() 并重用
  3. 定时器/打点器触发后,channel 会接收到一个时间值,但通常我们只关心触发事件本身

这些模式使得 Go 程序能够优雅地处理时间相关的操作,特别是在并发环境中非常有用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/919632.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/919632.shtml
英文地址,请注明出处:http://en.pswp.cn/news/919632.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

开源和免费一样吗?以商城系统为例为您分析~

开源和免费并不完全一样&#xff0c;二者在核心定义、权利范围和实际应用中存在显著区别&#xff0c;具体可以从以下几个方面理解&#xff1a; 1. 核心定义不同开源&#xff08;Open Source&#xff09;&#xff1a; 指软件的源代码是公开可获取的&#xff0c;任何人都可以查看…

CMOS知识点 MOS管饱和区电流公式

知识点16&#xff1a;同上篇一样&#xff0c;MOS管主要有3个工作区域&#xff1a;截止区&#xff08;Cut-off Region&#xff09;&#xff1a; < &#xff0c;没有沟道形成&#xff0c;几乎没有电流。线性区/三极管区&#xff08;Triode Region&#xff09;&#xff1a; &g…

【集合框架LinkedList底层添加元素机制】

在 Java 集合框架中&#xff0c;LinkedList 与 ArrayList 是两种截然不同的线性表实现。如果说 ArrayList 像一个可以伸缩的“盒子阵列”&#xff0c;那么 LinkedList 就像一条由“节点”串联而成的“双向链条”。今天&#xff0c;我们将深入 LinkedList 的源码&#xff0c;一步…

《P2700 逐个击破》

题目背景三大战役的平津战场上&#xff0c;傅作义集团在以北平、天津为中心&#xff0c;东起唐山西至张家口的铁路线上摆起了一字长蛇阵&#xff0c;并企图在溃败时从海上南逃或向西逃窜。为了就地歼敌不让其逃走&#xff0c;指挥官制定了先切断敌人东西两头退路然后再逐个歼灭…

C6.0:晶体管放大器的原理与应用(基极偏置篇)

将晶体管Q点偏置在负载线中点附近后&#xff0c;如果将一个小的交流信号耦合到基极上&#xff0c;便会产生一个交流的集电极电压&#xff0c;交流集电极电压与交流基极电压波形相似&#xff0c;但是幅度要大了很多&#xff0c;即交流集电极电压是对交流基极电压的放大。本篇学习…

Oracle: cannot decrease column length because some value is too big

1.背景今天项目上查不到数据,查库发现默认20位的字段被改为了200,用的还是char类型&#xff0c;填充了一堆空格 2.知识LENGTH() 函数用于计算字符串字段 长度TRIM() 函数用于去除字符串字段 column 前后的空格&#xff08;默认&#xff09;或指定字符&#xff1a;SUBSTR() 用于…

Elasticsearch 写入全链路:从单机到集群

0. 先把术语摆正 Index&#xff08;索引&#xff09;&#xff1a;逻辑数据集合&#xff0c;≈ MySQL 的库。Document&#xff08;文档&#xff09;&#xff1a;一条 JSON 数据&#xff0c;≈ MySQL 的行。Field&#xff08;字段&#xff09;&#xff1a;文档里的键值&#xff0…

Java多线程编程——基础篇

目录 前言 一、进程与线程 1、进程 2、线程 二、并发与并行 1、并发 2、并行 三、线程调度 1、CPU时间片 2、调度方式 ①时间片轮转 ②抢占式调度 四、线程实现方式 1、继承 Thread 类 Thread的多种构造函数&#xff1a; 2、实现 Runnable 接口 五、线程的核心方法 1、start() …

阿里云的centos8 服务器安装MySQL 8.0

在 CentOS 8 上安装 MySQL 8.0 可以通过添加 MySQL 官方 YUM 仓库并使用 dnf 命令安装。以下是具体步骤&#xff1a; 步骤如下&#xff1a; 下载并添加 MySQL 官方 YUM 仓库 运行以下命令下载 MySQL 8.0 的 YUM 仓库配置文件&#xff1a; sudo dnf install https://dev.mysql.…

【运维进阶】Linux 正则表达式

Linux 正则表达式定义&#xff1a;正则表达式是一种pattern&#xff08;模式&#xff09;&#xff0c;用于与待搜索字符串匹配&#xff0c;以查找一个或多个目标字符串。组成&#xff1a;自成体系&#xff0c;由两类字符构成普通字符&#xff1a;未被显式指定为元字符的所有可打…

STM32输入捕获相位差测量技术详解(基于TIM1复位模式)

本文将深入解析基于STM32定时器输入捕获功能的方波相位差测量技术&#xff0c;通过复位模式实现高精度相位检测。以下是完整的代码实现与详细原理分析。一、相位差测量原理相位差测量基于两个同频方波信号下降沿时间差计算。核心原理&#xff1a;​复位模式​&#xff1a;将TIM…

什么是股指期货可转移阿尔法策略?

阿尔法&#xff08;Alpha&#xff09;是投资领域的一个术语&#xff0c;用来衡量投资组合的超额收益。简单来说&#xff0c;阿尔法就是你在市场上赚的比平均水平多出来的那部分钱。比如&#xff0c;市场平均收益率是5%&#xff0c;但你的投资组合收益率是10%&#xff0c;那你的…

AXI GPIO S——ZYNQ学习笔记10

AXI GPIO 同意通道混合输入输出中断控制#KEY set_property IOSTANDARD LVCMOS18 [get_ports {AXI_GPIO_KEY_tri_io[0]}] set_property PACKAGE_PIN J13 [get_ports {AXI_GPIO_KEY_tri_io[0]}] set_property IOSTANDARD LVCMOS18 [get_ports {AXI_GPIO_KEY_tri_io[1]}] set_pro…

如何通过传感器选型优化,为设备寿命 “续航”?

在当今竞争激烈的工业领域&#xff0c;企业就像在一场没有硝烟的战争中角逐&#xff0c;设备便是企业的“秘密武器”。设备的使用寿命&#xff0c;如同武器的耐用程度&#xff0c;直接决定了企业在生产战场上的“战斗力”。延长设备寿命&#xff0c;已然成为众多企业降低生产成…

WebSocket连接的例子

// 初始化WebSocket连接 const initWebSocket () > {console.log("初始化链接中...")const websocketUrl ws://61.54.84.16:9090/;// WebSocket服务器地址websocket new WebSocket(websocketUrl)//使用真实的webscket// websocket new MockWebSocket(websocket…

c++之指针和引用

一 使用场景 C++ 什么时候使用指针?什么时候使用引用?什么时候应该按值传递?_引用什么时候用比较好-CSDN博客 只使用传递过来的值,而不对值进行修改 需要修改传递过来的值 内置数据类型 按值传递(小型结构) 指针传递 数组 指针传递 指针传递 结构 指针或引用(较大的结构…

pytorch学习笔记-模型训练、利用GPU加速训练(两种方法)、使用模型完成任务

应该算是完结啦~再次感谢土堆老师&#xff01; 模型训练 模型训练基本可以分为以下几个步骤按序执行&#xff1a; 引入数据集-使用dataloader加载数据集-建立模型-设置损失函数-设置优化器-进行训练-训练中计算损失&#xff0c;并使用优化器更新参数-模型测试-模型存储 习惯上会…

深度卷积神经网络AlexNet

在提出LeNet后卷积神经网络在计算机视觉和机器学习领域中报有名气&#xff0c;但是卷积神经网络并没有主导这些领域&#xff0c;因为LeNet在小数据集上取得了很好的效果&#xff0c;在更大&#xff0c;更真实的数据集上训练卷积神经网络的性能 和可行性有待研究&#xff0c;20世…

数据结构-HashSet

在 Java 编程的世界里&#xff0c;集合框架是极为重要的一部分&#xff0c;而 HashSet 作为 Set 接口的典型实现类&#xff0c;在处理不允许重复元素的场景中频繁亮相。今天&#xff0c;我们就一同深入探究 HashSet&#xff0c;梳理它的特点、常用方法&#xff0c;以及和其他相…

心意行药号 · 慈心方的八种用法

心意行药号 慈心方的八种用法慈心方是心意行药号589个珍贵秘方中的一个养生茶方&#xff0c;配伍比例科学严谨&#xff0c;君臣佐使堪称经典&#xff0c;自古就有“小小慈心方&#xff0c;转动大乾坤”之说。自清代光绪年间传承至今&#xff0c;慈心方受益者逾百万计&#xff…