Go 语言核心 36 讲
郝林
《Go 并发编程实战》作者,前轻松筹大数据负责人
79610 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 55 讲
Go 语言核心 36 讲
15
15
1.0x
00:00/00:00
登录|注册

10 | 通道的基本操作

缓冲通道
非缓冲通道
非缓冲通道
缓冲通道
使用操作符<-
容量
使用make函数
引发panic
长时间阻塞
阻塞
不可分割性
互斥性
示例解析
发送和接收操作
先进先出(FIFO)队列
初始化
并发安全性
元素值的复制方式
通道长度的含义
并发编程模式
基本特性的清楚
初始化通道的意义
错误使用通道
知识扩展
基本特性
示例代码
数据传递方式
通道类型
思考题
总结
问题解析
通道的基本操作

该思维导图由 AI 生成,仅供参考

作为 Go 语言最有特色的数据类型,通道(channel)完全可以与 goroutine(也可称为 go 程)并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学。
Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)
这是作为 Go 语言的主要创造者之一的 Rob Pike 的至理名言,这也充分体现了 Go 语言最重要的编程理念。而通道类型恰恰是后半句话的完美实现,我们可以利用通道在多个 goroutine 之间传递数据。

前导内容:通道的基础知识

通道类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类型。它使用起来十分简单,并不会徒增我们的心智负担。
在声明并初始化一个通道的时候,我们需要用到 Go 语言的内建函数make就像用make初始化切片那样,我们传给这个函数的第一个参数应该是代表了通道的具体类型的类型字面量。
在声明一个通道类型变量的时候,我们首先要确定该通道类型的元素类型,这决定了我们可以通过这个通道传递什么类型的数据。
比如,类型字面量chan int,其中的chan是表示通道类型的关键字,而int则说明了该通道类型的元素类型。又比如,chan string代表了一个元素类型为string的通道类型。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Go语言中的通道(channel)是一种独特的并发编程模式,与goroutine并驾齐驱,代表了Go语言独有的并发编程模式和编程哲学。通道类型的值本身是并发安全的,使用起来简单,不会增加心智负担。通过内建函数`make`声明并初始化通道,确定通道类型的元素类型,以及通道的容量。通道分为非缓冲通道和缓冲通道,具有不同的数据传递方式。发送和接收操作对元素值的处理是不可分割的,操作之间是互斥的,并且在完成之前会被阻塞。通道操作的阻塞实际上是为了实现操作的互斥和元素值的完整。通道的基本特性包括发送操作和接收操作之间的互斥性,对元素值的处理是不可分割的,以及操作在完成之前会被阻塞。通道的使用可以帮助实现并发编程,体现了Go语言的并发编程模式和编程哲学。 通道的长度代表着通道中当前存储的元素数量,而通道的容量则代表了通道可以存储的元素数量上限。元素值在经过通道传递时会被复制,这是浅表复制。发送操作和接收操作在特定情况下会被长时间的阻塞,如通道已满或已空时。同时,对于一个已初始化但未关闭的通道来说,收发操作一定不会引发panic,但一旦通道关闭,再对其进行发送操作会引发panic。因此,正确使用通道的操作规则和特性对于并发编程至关重要。 总的来说,通道是Go语言并发编程中的重要组成部分,具有独特的特性和行为模式,需要程序员深入理解并熟练运用。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Go 语言核心 36 讲》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(77)

  • 最新
  • 精选
  • 忘怀
    Go里没有深copy。 即便有的话这里可能也不会用吧,创建一个指针的内存开销绝大多数情况下要比重新开辟一块内存再把数据复制过来好的多吧。 老师,这么说对吗?

    作者回复: 对,这就是传指针值的好处之一。

    2018-09-12
    65
  • colonel
    通道底层存储数据的是链表还是数组?

    作者回复: 环形链表

    2018-09-23
    2
    48
  • 江山如画
    老师回复我后突然感觉不对劲,结构体是值类型,通道传输的时候会新拷贝一份对象,底层数据结构会被复制,引用类型可能就不一定了,又用数组和切片试了下,发现切片在通道传输的时候底层数据结构不会被复制,改了一个另外一个也会跟着改变,所以切片这里应该是浅复制,数组一个改了对另一个没有影响是深层复制,代码: // ch := make(chan []int, 1) s1 := []int{1, 2, 3} ch <- s1 s2 := <-ch s2[0] = 100 fmt.Println(s1, s2) //[100 2 3] [100 2 3] // ch2 := make(chan [3]int, 1) s3 := [3]int{1, 2, 3} ch2 <- s3 s4 := <-ch2 s3[0] = 100 fmt.Println(s3, s4) //[100 2 3] [1 2 3]

    作者回复: 再说一遍,Go语言里没有深层复制。数组是值类型,所以会被完全复制。

    2018-09-03
    13
    36
  • melon
    感觉chanel有点像socket的同步阻塞模式,只不过channel的发送端和接收端共享一个缓冲,套接字则是发送这边有发送缓冲,接收这边有接收缓冲,而且socket接收端如果先close的话,发送端再发送数据的也会引发panic(linux上会触发SIG_PIPE信号,不处理程序就崩溃了)。 另使用demo21.go测试发送接收阻塞情况时需要额外空跑一个goroutine,否则会引发这样的panic(至 少1.11版是这样):fatal error: all goroutines are asleep - deadlock!

    作者回复: 对,所以注释中才会那么说。

    2018-09-03
    28
  • 来碗绿豆汤
    深copy还是浅copy,跟具体数据类型有关,引用型数据就是浅copy,数值型数据就是深copy.如,如果是切片类型则是浅copy,如果是数组则是深copy

    作者回复: 其实都是浅表复制。数组因为是值类型的,所以即使是浅复制也能完全复制过来。

    2018-09-03
    2
    20
  • 会哭的鱼
    老师您好,通道这里我看了好几遍了,对于评论中有一个问题一直不明白,非常希望老师能够解答一下! 同学阿拉丁的瓜的提问: 请问老师,缓冲通道内的值是被并行读出的吗? 比如两个goroutine分别为r1和r2;一个装满的容量为2的chan。 当r1正在取出先入的数据时,r2是否可以取出后入的数据;还是说r2必须阻塞,等到先入数据被完全取走之后才能开始读取后入的数据? 老师回答: 作者回复: 可以同时进行,通道是并发安全的。但是不一定哪个g拿到哪个元素值。 个人不明白,按照我看完的理解,同一个通道不管有多少并发在接收操作,同一个通道同时只能被一个goroutine操作,其他的都要在这个接收操作完成 “复制通道内的元素值”“放置副本到接收方”“删掉原值”三步完全完成后才可以继续进行的,负责就要一直阻塞才对 老师原文中是这样的: 类似的,在同一时刻,运行时系统也只会执行,对同一个通道的任意个接收操作中的某一个。 直到这个元素值完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。即使这些操作是并发执行的也是如此。 这里所谓的并发执行,你可以这样认为,多个代码块分别在不同的 goroutine 之中,并有机会在同一个时间段内被执行。 请老师解答一下我这个疑惑,非常感谢!

    作者回复: 你理解的没错,在同一时刻,只有一个goroutine能够对某一个通道进行取出操作,其他的试图对这个通道进行取出操作的goroutine都会被阻塞,并进入通道内部的队列排队。通道会保证这种操作是互斥的,并且是原子性的(完全取走一个元素值之后,下一个元素值才有可能被取)。 我回答那位同学的意思是:两个go函数中的代码是有可能同时(在同一个很小的时间段内)执行到“取出操作”那一行代码的。不过我们完全不用在意,因为通道和运行时系统会保证这类操作的并发安全。 可能我那个回答太短了吧,咱俩没有对上口径。

    2019-03-03
    5
    15
  • My dream
    老师,请教一下,通道的传值可以直接传指针不,你讲的拷贝,那么内存开销是很大的,如果通道传指针的话,会不会好很多

    作者回复: 原则上可以传任何类型的数据。不过,要是传指针的话要自己保证安全啊,原始数据放篡改之类的。

    2018-09-21
    2
    13
  • 请叫我小岳同学
    1. 通道的长度,表示channel 缓冲的长度。当channel处于阻塞状态时,容纳最多的同类型的长度。 2. 深拷贝

    作者回复: 第一个问题,长度代表通道当前包含的元素个数,容量就是初始化时你设置的那个数。 第二个问题你再想想,可以做做试验。

    2018-09-03
    11
  • Geek_a8be59
    ch1 := make(chan int, 2) // 发送方。 go func() { for i := 0; i < 10; i++ { fmt.Printf("Sender: sending element %v...\n", i) ch1 <- i } fmt.Println("Sender: close the channel...") close(ch1) }() // 接收方。 for { elem, ok := <-ch1 if !ok { fmt.Println("Receiver: closed channel") break } fmt.Printf("Receiver: received an element: %v\n", elem) } fmt.Println("End.") 老师,根据您的提供的源码有三个问题需请教。 第一问题:第一次携程调度应该发生主携程中的elem, ok := <-ch1 这个代码处,这时候应该在chan有等待的协成,再第一向chan1<-i传值得时候,根据您的描述"当发送操作在执行的时候发现空的通道中,正好有等待的接收,那么会把元素直接复制给对方"。照这么说应该在这次就跳转到主协成中,并打印出接收到的数据了。但是实际是先发送i=3的时候才做第一次携程调度,请问这是为什么? 第二问题:缓存区的大小不是设置的是2么,为什么length当发送了i=3的时候才会阻塞发生调度呢,正常不是应该i=2的时候么 第三个问题:当for循环结束了以后 就是在chan关闭之前,为什么又能调度到主协成让他接收呢。不应该到这个协成调用结束么?

    作者回复: 所以说不要用“协程”这个概念,因为“协程(coroutine)”指的是程序在同一个线程内的自行调度,是应用程序本身完全可控的。而 goroutine 的调度是 Go 语言的运行时系统发起的。 你不要揣测 Go 语言的调度器会怎样调度。你首先要知道哪些代码点是调度的时机(注意,到了调度时机也不一定发生调度,只是时机而已)。你还要知道如果想让多个 goroutine 按照你拟定的流程执行就需要用到 Channel 以及各种同步工具。 你说的“跳转到”只能在 coroutine 场景下才能这么说。在 goroutine 的场景下,没有“跳转”这么一说。 其一,你在上面的 for 语句中启用了一个 goroutine,你怎么就能断定后面的代码一定会先于这个 go 函数执行?不要做这种假设。因为连 goroutine 的调度都是并发的。 其二,两个 goroutine 一个 channel,一个 goroutine 发,一个 goroutine 取。这个 ch1 什么时候满、什么时候空,你基本上是确定不了的。因为两个 for 循环 在迭代的过程中都可能因被调用而换下 CPU。 其三,你要知道,几乎任何函数调用都存在调度时机,更何况是像 fmt.Println 这种需要 I/O 的重型操作。所以,为什么你那前一个 for 循环结束之后就不能被调度了呢? 以上是我通过你的文字表达猜测并回答的,并不一定完全匹配你要问的问题。还有问题的话再问我。 我觉得你对“并发”和“调度”这两个概念不清楚。我建议你好好看看专栏里讲 goroutine 的那几篇文章。有必要的话,买我的《Go 并发编程实战》第二版从头学一下。

    2019-06-27
    3
    10
  • Yayu
    老师,我知道 golang 这门语言中所有的变量赋值操作都是 value copy的,不论这个变量是值类型,还是指针类型。关于您这里说的 shallow copy 与 deep copy 的问题我还是不是很清楚, google 了一下,每门语言的支持都不太一样,您是怎么定义这两个概念的?能否详细说一下?

    作者回复: 浅拷贝只是拷贝值以及值中直接包含的东西,深拷贝就是把所有深层次的结构一并拷贝。

    2018-09-05
    10
收起评论
显示
设置
留言
77
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部