golang 中 channel 的详细使用、使用注意事项及死锁问题解析
本篇文章向大家介绍《golang 中 channel 的详细使用、使用注意事项及死锁问题解析》,主要包括channel,具有一定的参考价值,需要的朋友可以参考一下。
什么是 channel 管道
它是一个数据管道,可以往里面写数据,从里面读数据。
channel 是 goroutine 之间数据通信桥梁,而且是线程安全的。
channel 遵循先进先出原则。
写入,读出数据都会加锁。
channel 可以分为 3 种类型:
- 只读 channel,单向 channel
- 只写 channel,单向 channel
- 可读可写 channel
channel 还可按是否带有缓冲区分为:
带缓冲区的 channel,定义了缓冲区大小,可以存储多个数据
不带缓冲区的 channel,只能存一个数据,并且只有当该数据被取出才能存下一个数据
channel 的基本使用
定义和声明
// 只读 channel var readOnlyChan <p>chan_var.go</p> <pre class="brush:plain;">package main import ( "fmt" ) func main() { // var 声明一个 channel,它的零值是nil var ch chan int fmt.Printf("var: the type of ch is %T \n", ch) fmt.Printf("var: the val of ch is %v \n", ch) if ch == nil { // 也可以用make声明一个channel,它返回的值是一个内存地址 ch = make(chan int) fmt.Printf("make: the type of ch is %T \n", ch) fmt.Printf("make: the val of ch is %v \n", ch) } ch2 := make(chan string, 10) fmt.Printf("make: the type of ch2 is %T \n", ch2) fmt.Printf("make: the val of ch2 is %v \n", ch2) } // 输出: // var: the type of ch is chan int // var: the val of ch is <nil> // make: the type of ch is chan int // make: the val of ch is 0xc000048060 // make: the type of ch2 is chan string // make: the val of ch2 is 0xc000044060</nil>
操作channel的3种方式
操作 channel 一般有如下三种方式:
- 读
- 写 ch
- 关闭 close(ch)
操作 | nil的channel | 正常channel | 已关闭的channel |
---|---|---|---|
读 | 阻塞 | 成功或阻塞 | 读到零值 |
写 ch | 阻塞 | 成功或阻塞 | panic |
关闭 close(ch) | panic | 成功 | panic |
注意 对于 nil channel 的情况,有1个特殊场景:
当 nil channel 在 select 的某个 case 中时,这个 case 会阻塞,但不会造成死锁。
单向 channel
单向 channel:只读和只写的 channel
chan_uni.go
package main import "fmt" func main() { // 单向 channel,只写channel ch := make(chan <p>把上面代码main()函数里初始化的单向channel,修改为可读可写channel,再运行</p> <p>chan_uni2.go</p> <pre class="brush:plain;">package main import "fmt" func main() { // 把上面代码main()函数初始化的单向 channel 修改为可读可写的 channel ch := make(chan int) go testData(ch) fmt.Println( <h3>带缓冲和不带缓冲的 channel</h3> <h4>不带缓冲区 channel</h4> <p>chan_unbuffer.go</p> <pre class="brush:plain;">package main import "fmt" func main() { ch := make(chan int) // 无缓冲的channel go unbufferChan(ch) for i := 0; i <h4>带缓冲区 channel</h4> <p>chan_buffer.go</p> <pre class="brush:plain;">package main import ( "fmt" ) func main() { ch := make(chan string, 3) ch <p>再看一个例子:chan_buffer2.go</p> <pre class="brush:plain;">package main import ( "fmt" "time" ) var c = make(chan int, 5) func main() { go worker(1) for i := 1; i <h3>判断 channel 是否关闭</h3> <pre class="brush:plain;">if v, ok := <p>说明:</p>
- ok 为 true,读到数据,且管道没有关闭
- ok 为 false,管道已关闭,没有数据可读
读已经关闭的 channel 会读到零值,如果不确定 channel 是否关闭,可以用这种方法来检测。
range and close
range 可以遍历数组,map,字符串,channel等。
一个发送者可以关闭 channel,表明没有任何数据发送给这个 channel 了。接收者也可以测试channel是否关闭,通过 v, ok := 表达式中的 ok 值来判断 channel 是否关闭。上一节已经说明 ok 为 false 时,表示 channel 没有接收任何数据,它已经关闭了。
注意:仅仅只能是发送者关闭一个 channel,而不能是接收者。给已经关闭的 channel 发送数据会导致 panic。
Note: channels 不是文件,你通常不需要关闭他们。那什么时候需要关闭?当要告诉接收者没有值发送给 channel 了,这时就需要了。
比如终止 range 循环。
当 for range 遍历 channel 时,如果发送者没有关闭 channel 或在 range 之后关闭,都会导致 deadlock(死锁)。
下面是一个会产生死锁的例子:
package main import "fmt" func main() { ch := make(chan int) go func() { for i := 0; i <p>改正也很简单,把 <code>close(ch)</code> 移到 <code>go func(){}()</code> 里,代码如下</p> <pre class="brush:plain;">go func() { for i := 0; i <p>这样程序就可以正常运行,不会报 deadlock 的错误了。</p> <p>把上面程序换一种方式来写,chan_range.go</p> <pre class="brush:plain;">package main import ( "fmt" ) func main() { ch := make(chan int) go test(ch) for val := range ch { // fmt.Println("get val: ", val) } } func test(ch chan int) { for i := 0; i <p>发送者关闭 channel 时,for range 循环自动退出。</p> <h3>for 读取channel</h3> <p>用 for 来不停循环读取 channel 里的数据。</p> <p>把上面的 range 程序修改下,chan_for.go</p> <pre class="brush:plain;">package main import ( "fmt" ) func main() { ch := make(chan int) go test(ch) for { val, ok := <h3>select 使用</h3> <p>例子 chan_select.go</p> <pre class="brush:plain;">package main import "fmt" // https://go.dev/tour/concurrency/5 func fibonacci(ch, quit chan int) { x, y := 0, 1 for { select { case ch <h2>channel 的一些使用场景</h2> <h3>1. 作为goroutine的数据传输管道</h3> <pre class="brush:plain;">package main import "fmt" // https://go.dev/tour/concurrency/2 func sums(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <p>用 goroutine 和 channel 分批求和</p> <h3>2. 同步的channel</h3> <p>没有缓冲区的 channel 可以作为同步数据的管道,起到同步数据的作用。</p> <p>对没有缓冲区的 channel 操作时,发送的 goroutine 和接收的 goroutine 需要同时准备好,也就是发送和接收需要一一配对,才能完成发送和接收的操作。</p> <p>如果两方的 goroutine 没有同时准备好,channel 会导致先执行发送或接收的 goroutine 阻塞等待。这就是没有缓冲区的 channel 作为数据同步的作用。</p> <p><a target='_blank' href='https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyero5kcM7Ji4eYkbqVsJqthaW7ZGmosWuGn4mQbq2_ut2sl2Wj2r1mmd2alZmpndN2YMOJaamusoWaebKHo7-3t2yBdonRs4iDz4XNqWyR032psYaXabSAjap-a4ayspW7aY2skc-zhW6h' rel='nofollow'>gobyexample</a> 中的一个例子:</p> <pre class="brush:plain;">package main import ( "fmt" "time" ) //https://gobyexample.com/channel-synchronization func worker(done chan bool) { fmt.Println("working...") time.Sleep(time.Second) fmt.Println("done") done <p>注意:同步的 channel 千万不要在同一个 goroutine 协程里发送和接收数据。可能导致deadlock死锁。</p> <h3>3. 异步的channel</h3> <p>有缓冲区的 channel 可以作为异步的 channel 使用。</p> <p>有缓冲区的 channel 也有操作注意事项:</p> <p>如果 channel 中没有值了,channel 为空了,那么接收者会被阻塞。</p> <p>如果 channel 中的缓冲区满了,那么发送者会被阻塞。</p> <p>注意:有缓冲区的 channel,用完了要 close,不然处理这个channel 的 goroutine 会被阻塞,形成死锁。</p> <pre class="brush:plain;">package main import ( "fmt" ) func main() { ch := make(chan int, 4) quitChan := make(chan bool) go func() { for v := range ch { fmt.Println(v) } quitChan <h3>4.channel 超时处理</h3> <p>channel 结合 time 实现超时处理。</p> <p>当一个 channel 读取数据超过一定时间还没有数据到来时,可以得到超时通知,防止一直阻塞当前 goroutine。</p> <p>chan_timeout.go</p> <pre class="brush:plain;">package main import ( "fmt" "time" ) func main() { ch := make(chan int) quitChan := make(chan bool) go func() { for { select { case v := <h2>使用 channel 的注意事项及死锁分析</h2> <h3>未初始化的 channel 读写关闭操作</h3> <p>1.读:未初始化的channel,读取里面的数据时,会造成死锁deadlock</p> <pre class="brush:plain;">var ch chan int <p>2.写:未初始化的channel,往里面写数据时,会造成死锁deadlock</p> <pre class="brush:plain;">var ch chan int ch <p>3.关闭:未初始化的channel,关闭该channel时,会panic</p> <pre class="brush:plain;">var ch chan int close(ch) // 关闭未初始化channel,触发panic
已初始化的 channel 读写关闭操作
1. 已初始化,没有缓冲区的channel
// 代码片段1 func main() { ch := make(chan int) ch <p>代码片段1:没有缓冲channel,且只有写入没有读取,会产生死锁</p> <pre class="brush:plain;">// 代码片段2 func main() { ch := make(chan int) val, ok := <p>代码片段2:没有缓冲channel,且只有读取没有写入,会产生死锁</p> <pre class="brush:plain;">// 代码片段3 func main() { ch := make(chan int) val, ok := <p>代码片段3:没有缓冲channel,既有写入也有读出,但是在代码 <code>val, ok := 处已经产生死锁了。下面代码执行不到。</code></p> <pre class="brush:plain;">// 代码片段4 func main() { ch := make(chan int) ch <p>代码片段4:没有缓冲channel,既有写入也有读出,但是运行程序后,报错 <code>fatal error: all goroutines are asleep - deadlock!</code> 。</p> <p>这是因为往 channle 里写入数据的代码 <code>ch ,这里写入数据时就已经产生死锁了。把 <code>ch 和 <code>go readChan(ch)</code> 调换位置,程序就能正常运行,不会产生死锁。</code></code></p> <pre class="brush:plain;"> // 代码片段5 func main() { ch := make(chan int) go writeChan(ch) for { val, ok := <p>代码片段5:没有缓冲的channel,既有写入,也有读出,与上面几个代码片段不同的是,写入channel的数据不是一个。</p> <p>思考一下,这个程序会产生死锁吗?10 秒时间思考下,先不要看下面。</p> <p>也会产生死锁,它会输出完数据后,报错 <code>fatal error: all goroutines are asleep - deadlock!</code>。</p> <p>为什么呢?这个程序片段,既有读也有写而且先开一个goroutine写数据,为什么会死锁?</p> <p>原因在于 <code>main()</code> 里的 <code>for</code> 循环。可能你会问,不是有 <code>break</code> 跳出 <code>for</code> 循环吗?代码是写了,但是程序并没有执行到这里。</p> <p>因为 <code>for</code> 会不停的循环,而 <code>val, ok := , 这里 <code>ok</code> 值一直是 true,因为程序里并没有哪里关闭 channel 啊。你们可以打印这个 <code>ok</code> 值看一看是不是一直是 true。当 <code>for</code> 循环把 channel 里的值读取完了后,程序再次运行到 <code>val, ok := 时,产生死锁,因为 channel 里没有数据了。</code></code></p> <p>找到原因了,那解决办法也很简单,在 <code>writeChan</code> 函数里关闭 channel,加上代码 <code>close(ch)</code>。告诉 <code>for</code> 我写完了,关闭 channel 了。</p> <p>加上关闭 channel 代码后运行程序:</p> <pre class="brush:plain;">read ch: 0 , ok: true read ch: 1 , ok: true read ch: 2 , ok: true read ch: 3 , ok: true read ch: 0 , ok: false end
程序正常输出结果。
对于没有缓冲区的 channel (unbuffered channel) 容易产生死锁的几个代码片段分析,总结下:
- channel 要用 make 进行初始化操作
- 读取和写入要配对出现,并且不能在同一个 goroutine 里
- 一定先用 go 起一个协程执行读取或写入操作
- 多次写入数据,for 读取数据时,写入者注意关闭 channel(代码片段5)
2. 已初始化,有缓冲区的 channel
// 代码片段1 func main() { ch := make(chan int, 1) val, ok := <p>代码片段1:有缓冲channel,先读数据,这里会一直阻塞,产生死锁。</p> <pre class="brush:plain;">// 代码片段2 func main() { ch := make(chan int, 1) ch <p>代码片段2:同代码片段1,有缓冲channel,只有写没有读,也会阻塞,产生死锁。</p> <pre class="brush:plain;"> // 代码片段3 func main() { ch := make(chan int, 1) ch <p>代码片段3:有缓冲的channel,有读有写,正常的输出结果。</p> <p>有缓冲区的channel总结:</p>
- 如果 channel 满了,发送者会阻塞
- 如果 channle 空了,接收者会阻塞
- 如果在同一个 goroutine 里,写数据操作一定在读数据操作前
参考
https://go.dev/tour/concurrency
https://go.dev/ref/spec#Channel_types
https://go.dev/ref/spec#Send_statements
https://go.dev/ref/spec#Receive_operator
https://go.dev/ref/spec#Close
https://go.dev/doc/effective_go#channels
https://go.dev/ref/spec#Select_statements
https://gobyexample.com/
Concurrency is not parallelism - The Go Programming Language
今天关于《golang 中 channel 的详细使用、使用注意事项及死锁问题解析》的内容介绍就到此结束,如果有什么疑问或者建议,可以在golang学习网公众号下多多回复交流;文中若有不正之处,也希望回复留言以告知!

- 上一篇
- gorm整合进go-zero的实现方法

- 下一篇
- 聊聊goxorm生成mysql的结构体问题
-
- Golang · Go教程 | 12分钟前 |
- Golangdefer作用:资源清理与错误处理结合
- 107浏览 收藏
-
- Golang · Go教程 | 28分钟前 |
- Go语言获取程序名:os.Args[0]与flag用法解析
- 275浏览 收藏
-
- Golang · Go教程 | 28分钟前 |
- Golang实现Base64编码工具教程
- 450浏览 收藏
-
- Golang · Go教程 | 32分钟前 |
- Golang如何实现100%测试覆盖?边界测试技巧分享
- 236浏览 收藏
-
- Golang · Go教程 | 37分钟前 |
- Golang搭建DNA序列分析工具链教程
- 162浏览 收藏
-
- Golang · Go教程 | 37分钟前 |
- Golang实现混沌工程:ChaosMesh实战教程
- 137浏览 收藏
-
- Golang · Go教程 | 38分钟前 |
- Golang提升DevOps配置检测,动态热加载演示
- 478浏览 收藏
-
- Golang · Go教程 | 40分钟前 |
- Go语言Map遍历优化技巧分享
- 480浏览 收藏
-
- Golang · Go教程 | 46分钟前 |
- Golang错误追踪:zap与堆栈集成教程
- 348浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 511次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 498次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- 千音漫语
- 千音漫语,北京熠声科技倾力打造的智能声音创作助手,提供AI配音、音视频翻译、语音识别、声音克隆等强大功能,助力有声书制作、视频创作、教育培训等领域,官网:https://qianyin123.com
- 94次使用
-
- MiniWork
- MiniWork是一款智能高效的AI工具平台,专为提升工作与学习效率而设计。整合文本处理、图像生成、营销策划及运营管理等多元AI工具,提供精准智能解决方案,让复杂工作简单高效。
- 89次使用
-
- NoCode
- NoCode (nocode.cn)是领先的无代码开发平台,通过拖放、AI对话等简单操作,助您快速创建各类应用、网站与管理系统。无需编程知识,轻松实现个人生活、商业经营、企业管理多场景需求,大幅降低开发门槛,高效低成本。
- 104次使用
-
- 达医智影
- 达医智影,阿里巴巴达摩院医疗AI创新力作。全球率先利用平扫CT实现“一扫多筛”,仅一次CT扫描即可高效识别多种癌症、急症及慢病,为疾病早期发现提供智能、精准的AI影像早筛解决方案。
- 98次使用
-
- 智慧芽Eureka
- 智慧芽Eureka,专为技术创新打造的AI Agent平台。深度理解专利、研发、生物医药、材料、科创等复杂场景,通过专家级AI Agent精准执行任务,智能化工作流解放70%生产力,让您专注核心创新。
- 95次使用
-
- 深入理解Golangchannel的应用
- 2023-01-27 200浏览
-
- GoLangchannel使用介绍
- 2022-12-22 440浏览
-
- Go语言面试题之select和channel的用法
- 2022-12-30 477浏览
-
- Go底层channel实现原理及示例详解
- 2022-12-24 399浏览
-
- Golang channel为什么不会阻塞的原因详解
- 2023-01-27 455浏览