文章目录

  • 1.简介
  • 2.缓冲 channel
  • 3.range 和 close 操作
  • 4.select 操作
  • 5.注意要点
  • 6.常见用法
  • 参考文献

1.简介

channel 提供了一种通信机制,通过发送和接收指定元素类型的值来完成并发执行函数间的通信。未初始化 channel 为 nil。

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType

操作符 <- 形似箭头,其指向是数据流的方向。如果在定义 channel 时不指定,则说明 channel 是双向的。信道可通过类型转换或赋值被强制为只发送或只接收。

chan T          // 可以被用来发送和接收类型 T 的值
chan<- float64  // 只能被用来发送浮点数
<-chan int      // 只能被用来接收整数

通过操作符 <- 向其发送或从其接收值。

ch <- v           // 将 v 送入 channel ch
v := <-ch       // 从 ch 接收,并且赋值给 v
v, ok := <-ch   // 成功取出 ok 为 true,如果 ch 已关闭,则 ok 为 false,v 为对应类型的零值

和 map 与 slice 一样,channel 使用前必须创建,未初始化的信道值为 nil。

ch := make(chan int)

默认情况下,在另一端准备好之前,发送和接收都会阻塞。这使得 goroutine 可以在没有明确的锁或竞态变量的情况下进行同步。

2.缓冲 channel

channel 有两种形式的,一种是无缓冲的,一个 Go 程向这个 channel 发送了消息后,会阻塞当前Go 程,直到其他 Go 程接收了这个 channel 中的消息。

channel 可以是带缓冲的,创建 channel 时可以指定缓冲的消息数量,当消息数量小于指定值时,不会出现阻塞,超过之后才会阻塞。为 make 提供第二个参数作为缓冲大小来初始化一个缓冲 channel:

ch := make(chan int, 100)

向缓冲 channel 发送数据的时候,只有在缓冲区满的时候才会阻塞。当缓冲区为空的时候接收会阻塞。

package mainimport "fmt"func main() {ch := make(chan int, 2)ch <- 1ch <- 2fmt.Println(<-ch)fmt.Println(<-ch)
}

运行输出:

1
2

3.range 和 close 操作

发送者可以 close 一个 channel 来表示再没有值会被发送了。接收者可以通过赋值语句的第二参数来测试 channel 是否被关闭。当没有值可以接收并且 channel 已经被关闭,那么经过

v, ok := <-ch

之后 ok 会被设置为 false,v 为对应类型的零值。

循环 for v := range c 会不断地从 channel 接收值,直到它被关闭。

注意: 关闭 channel 应该由发送者而不是接收者。向一个已经关闭的 channel 发送数据会引起 panic。

还要注意: channel 与文件不同,通常情况下无需关闭它们,当一个 channel 没有被任何协程用到后最终会被 GC 回收,只有在需要告诉接收者没有更多数据的时候才有必要进行关闭,例如中断一个 range。

4.select 操作

select 语句使得一个 goroutine 在多个通讯操作上等待。

select 会阻塞,直到条件分支中的某个可以继续执行,这时就会执行那个条件分支。当多个都准备好的时候,会随机选择一个。

package mainimport "fmt"func fibonacci(c, quit chan int) {x, y := 0, 1for {select {case c <- x:x, y = y, x+ycase <-quit:fmt.Println("quit")return}}
}func main() {c := make(chan int)quit := make(chan int)go func() {for i := 0; i < 10; i++ {fmt.Println(<-c)}quit <- 0}()fibonacci(c, quit)
}

运行输出:

0
1
1
2
3
5
8
13
21
34
quit

为了非阻塞的发送或者接收,可使用 default 分支。当 select 中的其他条件分支都没有准备好的时候,default 分支会被执行。

package mainimport ("fmt""time"
)func main() {tick := time.Tick(100 * time.Millisecond)boom := time.After(200 * time.Millisecond)for {select {case <-tick:fmt.Println("tick.")case <-boom:fmt.Println("BOOM!")returndefault:fmt.Println("default")time.Sleep(50 * time.Millisecond)}}
}

运行输出:

default
default
tick.
default
default
tick.
BOOM!

5.注意要点

channel 的三种状态不同操作有不同的结果:

操作 未关闭 已关闭 nil
发数据 阻塞或成功发送 panic 永久阻塞
取数据 阻塞或成功接收 成功接收或零值 永久阻塞
关闭 成功关闭 panic panic

从上面的表格得知,从 nil channel 中读取或写入会永久阻塞。读取不论形式,包括 comma ok 式也会阻塞。

// 写入永久阻塞
var ch chan string
ch <- "Go"// 读取永久阻塞
x := <-ch
x, ok := <-ch
for x := range ch {...
}

另外还需要知道:

  • Go 在语法上禁止关闭只读 channel,调用 close() 函数时会报类型不匹配的错误;
  • channel 的关闭原则:不要从接收端关闭 channel,也不要在多个并发发送端中关闭 channel,而是在唯一或最后一个发送端中关闭 channel;
  • Go 中没有提供判断信道是否关闭的函数,因此没有办法只判断 channel 是否关闭而不从中取值。

6.常见用法

(1)无缓冲 channel 充当条件变量实现 Go 程同步。

c := make(chan int)  // 分配一个信道
// 在Go程中启动排序。当它完成后,在信道上发送信号。
go func() {list.Sort()c <- 1  // 发送信号,什么值无所谓。
}()
doSomethingForAWhile()
<-c   // 阻塞等待排序结束,丢弃发来的值。

(2)带缓冲的信道充当信号量,例如限制吞吐量。

var sem = make(chan int, MaxOutstanding)func Serve(queue chan *Request) {for req := range queue {sem <- 1go func(req *Request) {process(req)<-sem}(req)}
}

(3)channel 充当消息队列实现消费者生产者模型。
通过 channel 可以比较方便的实现生产者消费者模型,开启一个生产者线程,一个消费者线程,生产者线程往 channel 中发送消息,同时阻塞,消费者线程阻塞等待获取 channel 中的消息,进行处理。当生产者在完成了所有的消息发送后,close channel 通知消费者线程退出。

func main() {ichan := make(chan int)// 生产者go func() {for i := 0; i < 3; i++ {ichan <- ifmt.Printf("write finish, value=%v\n", i)}close(ichan)}()// 消费者func() {for v := range ichan {fmt.Printf("read finish, value=%v\n", v)}}()
}

运行输出:

read finish, value=0
write finish, value=0
write finish, value=1
read finish, value=1
read finish, value=2
write finish, value=2

(4)channel 的超时处理。
利用 time 包可以实现 channel 的超时处理,当一个 channel 读取超过一定时间没有消息到来时,就可以得到超时通知处理,防止一直阻塞当前线程。

func main() {g, quit := make(chan int), make(chan bool)// 生产消息go func() {for i := 0; i < 3; i++ {g <- i}}()// 消费消息go func() {for {select {case v := <-g:fmt.Println(v)case <-time.After(time.Second * time.Duration(2)):quit <- truefmt.Println("超时,通知主线程退出")return}}}()// 阻塞主线程,等待消费线程结束<-quitfmt.Println("收到退出通知,主线程退出")
}

运行输出:

0
1
2
收到退出通知,主线程退出

(5)指定 channel 为输入或输出型。
创建或申明 channel 时可以在显示指定它是输入型还是输出型的,输入型则不能从中读取消息,否则编译报错,同理,输出型不能输入消息。

这样可以在编写代码时防书写错误导致程序一场。指定输入输出类型可以在方法参数时设定,那么它只在当前方法中会做输入输出限制,这样可以将错误提前暴露于编译期。

func main() {ch, quit := make(chan int), make(chan bool)// 输入型 channel 格式: inChan chan<- int,如果对其读取则编译报错go func(inChan chan<- int) {for i := 0; i < 5; i++ {inChan <- i}close(inChan)}(ch)// 输出型 channel 格式: inChan <-chan int,如果对其输入则编译报错go func(outChan <-chan int) {for v := range outChan {fmt.Printf("print out value=%v\n", v)}quit <- true}(ch)// 阻塞主线程,等待消费线程完成消费<-quitfmt.Println("收到退出通知,主线程退出")
}

输出运行:

print out value=0
print out value=1
print out value=2
print out value=3
print out value=4
收到退出通知,主线程退出

(6)使用 channel 监听指定信号。
可以创建一个 os.Signal 类型的 channel,同时通过 signal.Notify 来监听 os.Interrupt 这个中断信号,因此执行到<- quit时就会阻塞在这里,直到收到了 os.Interrupt 这个中断信号,比如按 Ctrl+C 中断程序的时候,主程序就会退出了。当然还可以监听其他信号,例如 os.Kill 等。

func main() {quit := make(chan os.Signal)signal.Notify(quit, os.Interrupt)fmt.Println("按 Ctrl+C 可退出程序")<-quitfmt.Println("主程序退出")
}

(7)channel 的关闭。

  • 使用 sync.once 在发送端保证只关闭一次。
type MyChannel struct {C    chan Tonce sync.Once
}func NewMyChannel() *MyChannel {return &MyChannel{C: make(chan T)}
}func (mc *MyChannel) SafeClose() {mc.once.Do(func(){close(mc.C)})
}
  • 也可以用 sync.Mutex 来避免多次关闭。
type MyChannel struct {C      chan Tclosed boolmutex  sync.Mutex
}func NewMyChannel() *MyChannel {return &MyChannel{C: make(chan T)}
}func (mc *MyChannel) SafeClose() {mc.mutex.Lock()if !mc.closed {close(mc.C)mc.closed = true}mc.mutex.Unlock()
}func (mc *MyChannel) IsClosed() bool {mc.mutex.Lock()defer mc.mutex.Unlock()return mc.closed
}
  • 多次关闭,捕获 panic。
func SafeClose(ch chan T) (justClosed bool) {defer func() {if recover() != nil {justClosed = false}}()// assume ch != nil hereclose(ch) // panic if ch is closedreturn true
}

我们应该要理解为什么 Go 不支持内置 SafeClose 函数来关闭 channel,原因在于并不推荐从接收端或者多个并发发送端关闭 channel。Golang 甚至禁止关闭只接收的 channel。


参考文献

Golang.A Tour of Go
Golang.Channel types
The Go Programming Language Specification.Receive operator
简书.Go的channel常见使用方式
简书.如何优雅地关闭Go channel
StackOverflow.How to check a channel is closed or not without reading it?

Golang channel 快速入门相关推荐

  1. golang beego快速入门示例(单文件hello.go)

    安装beego & bee $ go get -u github.com/beego/beego/v2 $ go get -u github.com/beego/bee/v2 hello.go ...

  2. golang泛型快速入门使用(go 1.18及以后版本)

    本文完全来源于官方文档,可放心食用,如果看得懂英文,建议直接参考官方文档(Tutorial: Getting started with generics) 1.上手使用  例如,有 string-&g ...

  3. 【Golang 快速入门】项目实战:即时通信系统

    Golang 快速入门 即时通信系统 - 服务端 版本一:构建基础 Server 版本二:用户上线功能 版本三:用户消息广播机制 版本四:用户业务层封装 版本五:在线用户查询 版本六:修改用户名 版本 ...

  4. 【Golang 快速入门】高级语法:反射 + 并发

    Golang 快速入门 Golang 进阶 反射 变量内置 Pair 结构 reflect 结构体标签 并发知识 基础知识 早期调度器的处理 GMP 模型 调度器的设计策略 并发编程 goroutin ...

  5. golang快速入门[8.3]-深入理解IEEE754浮点数

    前文 golang快速入门[1]-go语言导论 golang快速入门[2.1]-go语言开发环境配置-windows golang快速入门[2.2]-go语言开发环境配置-macOS golang快速 ...

  6. Flume快速入门(五):File Channel之重播(replay)

    当FlumeChannel启动时,或者故障恢复时,会经历一次重播(replay)过程,重播的目的就是还原上一次的"现场",当然,最主要的就是恢复FlumeEventQueue中的内 ...

  7. golang 模板引擎 html,Golang模板引擎快速入门教程

    Go语言内置了 text/template 和 html/template两个模板库,专门用于处理网页html模板. html/template 是在 text/template 模板库的基础上增加了 ...

  8. hyperledger/fabric-区块链快速入门教程+错误解决——良好用户体验

    hyperledger/fabric-区块链快速入门 目录 操作系统 软件安装 git 安装 go安装 docker安装 源码获取 hyperledger 环境配置 镜像获取 入门测试 fabric- ...

  9. 20小时快速入门go语言视频 - Day1

    20小时快速入门go语言视频 - Day1 一.第一个 Go 程序 1.1 入口 1.2 Golang 保留的关键字 1.3 Golang预定义标识符 二.数据类型 2.1 数据类型的作用 2.2 数 ...

最新文章

  1. 2009江民中国大陆地区计算机网络安全报告
  2. JS组件系列——Bootstrap Table 冻结列功能IE浏览器兼容性问题解决方案
  3. mysql怎么查看是否存在死锁_mysql怎么查看有没有死锁
  4. 让Windows2008R2也能进入手柄设置(游戏控制器设置)
  5. find the OPP in your life
  6. 第六课 多算法组合与模型调优
  7. selenium2 webdriver要点理解
  8. 【ROS】ros入门21讲(上)
  9. paip.asp 项目流程及管理工具总结
  10. 大学物理实验习题+答案/缓慢更新
  11. 2020年中级数据库系统工程师考试时间表与考试大纲
  12. Linux下pgadmin4启动报错,如何在UBUNTU 16.04上安装桌面模式中的PGADMIN 4
  13. ​​​​​​亲测有效|强制删除电脑上无法删除的文件和文件夹
  14. STM32L476低功耗—进入STOP2模式4s后LPTIM中断唤醒+功率实测
  15. linux shell搜索某个字符串,然后在后面加上字符串?字符串后面插入字符串?sed字符串后面插入字符串?...
  16. iOS 应用安装包瘦身
  17. HOW to BECOME a GOOD THEORETICAL PHYSICIST(转载的)
  18. Undefined symbols for architecture x86_64: in mac OS
  19. matlab积分泛函,泛函积分的数学方法概观.pdf
  20. 素民党的故事 (01) 什么是素民党

热门文章

  1. 开源审计的最佳时机是什么时候?
  2. 根文件系统移植之使用busybox
  3. [leetcode-83-Remove Duplicates from Sorted List]
  4. Wampserver_开启CURL
  5. WinForm多线程+委托防止界面假死
  6. keepalived配置参数官方文档中文翻译版
  7. [Java] 蓝桥杯ALGO-119 算法训练 寂寞的数
  8. L1-053 电子汪-PAT团体程序设计天梯赛GPLT
  9. L2-012. 关于堆的判断-PAT甲级真题(堆的建立,向上调整)
  10. 1001. 害死人不偿命的(3n+1)猜想 (15)-PAT乙级真题