Go语言核心36讲
郝林
《Go并发编程实战》作者,前轻松筹大数据负责人
立即订阅
24083 人已学习
课程目录
已完结 54 讲
0/4登录后,你可以任选4讲全文学习。
开篇词+学习路线 (3讲)
开篇词 | 跟着学,你也能成为Go语言高手
免费
预习篇 | 写给0基础入门的Go语言学习者
50 | 学习专栏的正确姿势
模块一:Go语言基础知识 (6讲)
01 | 工作区和GOPATH
02 | 命令源码文件
03 | 库源码文件
04 | 程序实体的那些事儿(上)
05 | 程序实体的那些事儿(中)
06 | 程序实体的那些事儿 (下)
模块二:Go语言进阶技术 (16讲)
07 | 数组和切片
08 | container包中的那些容器
09 | 字典的操作和约束
10 | 通道的基本操作
11 | 通道的高级玩法
12 | 使用函数的正确姿势
13 | 结构体及其方法的使用法门
14 | 接口类型的合理运用
15 | 关于指针的有限操作
16 | go语句及其执行规则(上)
17 | go语句及其执行规则(下)
18 | if语句、for语句和switch语句
19 | 错误处理(上)
20 | 错误处理 (下)
21 | panic函数、recover函数以及defer语句 (上)
22 | panic函数、recover函数以及defer语句(下)
模块三:Go语言实战与应用 (27讲)
23 | 测试的基本规则和流程 (上)
24 | 测试的基本规则和流程(下)
25 | 更多的测试手法
26 | sync.Mutex与sync.RWMutex
27 | 条件变量sync.Cond (上)
28 | 条件变量sync.Cond (下)
29 | 原子操作(上)
30 | 原子操作(下)
31 | sync.WaitGroup和sync.Once
32 | context.Context类型
33 | 临时对象池sync.Pool
34 | 并发安全字典sync.Map (上)
35 | 并发安全字典sync.Map (下)
36 | unicode与字符编码
37 | strings包与字符串操作
38 | bytes包与字节串操作(上)
39 | bytes包与字节串操作(下)
40 | io包中的接口和工具 (上)
41 | io包中的接口和工具 (下)
42 | bufio包中的数据类型 (上)
43 | bufio包中的数据类型(下)
44 | 使用os包中的API (上)
45 | 使用os包中的API (下)
46 | 访问网络服务
47 | 基于HTTP协议的网络服务
48 | 程序性能分析基础(上)
49 | 程序性能分析基础(下)
尾声与思考题答案 (2讲)
尾声 | 愿你披荆斩棘,所向无敌
新年彩蛋 | 完整版思考题答案
Go语言核心36讲
登录|注册

11 | 通道的高级玩法

郝林 2018-09-05
我们已经讨论过了通道的基本操作以及背后的规则。今天,我再来讲讲通道的高级玩法。
首先来说说单向通道。我们在说“通道”的时候指的都是双向通道,即:既可以发也可以收的通道。
所谓单向通道就是,只能发不能收,或者只能收不能发的通道。一个通道是双向的,还是单向的是由它的类型字面量体现的。
还记得我们在上篇文章中说过的接收操作符<-吗?如果我们把它用在通道的类型字面量中,那么它代表的就不是“发送”或“接收”的动作了,而是表示通道的方向。
比如:
var uselessChan = make(chan<- int, 1)
我声明并初始化了一个名叫uselessChan的变量。这个变量的类型是chan<- int,容量是1
请注意紧挨在关键字chan右边的那个<-,这表示了这个通道是单向的,并且只能发而不能收。
类似的,如果这个操作符紧挨在chan的左边,那么就说明该通道只能收不能发。所以,前者可以被简称为发送通道,后者可以被简称为接收通道。
注意,与发送操作和接收操作对应,这里的“发”和“收”都是站在操作通道的代码的角度上说的。
从上述变量的名字上你也能猜到,这样的通道是没用的。通道就是为了传递数据而存在的,声明一个只有一端(发送端或者接收端)能用的通道没有任何意义。那么,单向通道的用途究竟在哪儿呢?
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Go语言核心36讲》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(40)

  • 江山如画
    感觉方法应该挺多,就看解决的是不是优雅

    第一个问题:发现某个channel被关闭后,为了防止再次进入这个分支,可以把这个channel重新赋值成为一个长度为0的非缓冲通道,这样这个case就一直被阻塞了:
    for {
    select {
    case _, ok := <-ch1:
    if !ok {
    ch1 = make(chan int)
    }
    case ..... :
    ////
    default:
    ////
    }
    }

    第二个问题:可以用 break和标签配合使用,直接break出指定的循环体,或者goto语句直接跳转到指定标签执行

    break配合标签:

    ch1 := make(chan int, 1)
    time.AfterFunc(time.Second, func() { close(ch1) })
    loop:
    for {
    select {
    case _, ok := <-ch1:
    if !ok {
    break loop
    }
    fmt.Println("ch1")
    }
    }
    fmt.Println("END")

    goto配合标签:

    ch1 := make(chan int, 1)
    time.AfterFunc(time.Second, func() { close(ch1) })
    for {
    select {
    case _, ok := <-ch1:
    if !ok {
    goto loop
    }
    fmt.Println("ch1")
    }
    }
    loop:
    fmt.Println("END")
    2018-09-05
    2
    38
  • 任性😀
    demo24里边少了rand.Seed(time.Now().Unix()),不然每次随机数都是固定的顺序
    2018-09-25
    16
  • 笨笨
    谢谢赫老师今日分享,回答问题如下
    1.对于select中被close的channel判断其第二个boolean参数,如果是false则被关闭,那么赋值此channel为nil,那么每次到这个nil的channel就会阻塞,select会忽略阻塞的通道,如果再搭配上default就一定能保证不会被阻塞了。
    2.通过定义标签,配合goto或者break能实现在同一个函数内任意跳转,故可以跳出多层嵌套的循环。

    作者回复: 你的问题是什么?

    2018-09-05
    9
  • 阿海
    运行了demo25.go, 发现结果是No candidate case is selected,原因跟
    var channels = [3]chan int{
    nil,
    make(chan int, 1),
    nil,
    }
    有关,因为channels[0], channels[2]都是nil,所以select case时阻塞,而channels[1]初始化为无缓存channel,当没有从channels[1]取值时,select case阻塞,所以一轮下来,没有符合的条件case,只能运行case default了。
    2019-01-05
    1
    5
  • 左氧佛沙星人
    老师好,demo25中的这段代码我没看懂,不是这个匹配上了吗?为啥没有执行呢?我理解应该打印The second candidate case is selected.。。。。
    ```
    case getChan(1) <- getNumber(1):
    ```
    能指点一下吗?

    作者回复: 因为 make(chan int) 初始化的是不带缓冲的通道。非缓冲通道只有在收发双方都就绪的情况下才能传递元素值,否则就阻塞。

    2019-03-25
    4
  • zhaopan
    老师好:
    仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。
    当接收通道操作有多个满足条件时, 这里的所有case表达式都被求值完毕, 应该怎么理解?
    是多个case表达式都能接收到通道的数据么?
    如果都接收了, 随机选择一个分支去处理接收的通道数据. 那其他满足条件的case分支怎么执行到了?
    如果是外层加for循环, 重新select语句, 那上一次select的操作其他满足条件未被选择的case还能收到上一次的数据么?
    这里的原理是什么呢?

    作者回复: 这里只会检查一下接收操作或发送操作是否可以进行(是否不会被阻塞)。有兴趣的话可以看一下 runtime/select.go 中的 selectgo 函数的源码。

    2019-02-19
    4
  • 啵啵
    第一个问题,如果判断到chan关闭,即取到的第二个值为false。则将该chan赋值为nil。
    第二个问题,根据情况使用goto或者return。或者加一个是否结束的标识,goto然后用两个break。
    2018-09-11
    4
  • 癫狂的小兵
    请问当select语句发现多个分支满足条件时随机选择一个分支执行 那怎样让其他满足条件的分支执行呢? for 循环 等待下一次循环时再执行?

    作者回复: 放在for循环里每次也是随机的,不过可以用for循环,或者再次执行select语句。

    2018-09-05
    4
  • 🤔
    https://stackoverflow.com/questions/25469682/break-out-of-select-loop

    for-select break 方法。
    2019-02-10
    3
  • heha37
    当第二个boolean参数为false的时候,在相应的case中设置chan为nil零值,再次case求值的时候会遭遇阻塞,会屏蔽该case。

    作者回复: 是的。

    2018-11-05
    3
  • Kennedy
    通道的类型如果是方法,性能会差吗?
    2018-09-10
    3
  • 长杰
    函数定义,有的用首字母大写的命名规则,有的用驼峰命名规则,老师能介绍一下go语言编程的规范介绍吗?

    作者回复: 你可以看看我在前面的(程序实体相关的)文章中讲的。程序实体的名称如果首字母大写那么就说明是其访问权限是公开的,否则就是包级私有的。

    2019-01-13
    2
  • 到不了的塔
    郝老师,请问第一题的答案是啥,不知道怎么屏蔽分支呢

    作者回复: 设置为nil就可以了。

    2018-10-24
    2
  • 许森森
    1 发现channel是closed之后,重新make,使得为nil,保证一直 阻塞。

    i := 0
    for {
    select {
    case _, ok := <-c:
    if !ok {
    c = make(chan bool)
    fmt.Println("!ok")
    } else {
    fmt.Println("c:", c)
    }
    default:
    time.Sleep(1e9) // 等待1秒钟
    fmt.Println("default, ", i)
    i = i + 1
    if i > 3 {
    return
    }
    }
    }
    2
    return 直接退出程序
    break loop // 结合loop,会退出for循环
    goto end //结合end,跳转

    //在select语句与for语句联用时,怎样直接退出外层的for语句?
    tick := time.Tick(100 * time.Millisecond)
    boom := time.After(500 * time.Millisecond)
    //loop:
    for {
    select {
    case <-tick:
    fmt.Println("tick.")
    case <-boom:
    fmt.Println("BOOM!")
    // return //直接退出程序
    //break loop // 结合loop,会退出for循环
    goto end //结合end
    default:
    fmt.Println(" .")
    time.Sleep(50 * time.Millisecond)
    }
    }

    end:
    fmt.Println("END...")
    2018-09-14
    2
  • Tron
    请教老师一个问题, 如果我用context 取消一个正在执行的下载任务,形如:
    select {
        case i := <- jobs:
              downloadBigBigFile()
        case ctx <-Done
              return
    }

    当 父进程发出cancel 指令后, 能够取消downloadBigBigFile()里面运行的任务吗?

    作者回复: 首先修正一点,这与进程无关,与 goroutine 有关,而且没有父子关系。

    正题回答:不能。因为程序流程已经走到 downloadBigBigFile() 这里了,不可能在没有循环或跳转的情况下再往回走。

    看起来你应该把 ctx 放入 downloadBigBigFile 函数,然后在这个函数里做判断。

    2019-09-04
    1
  • Flo
    对zhaopan的问题中老师的回复存在疑惑,老师回复如下:“作者回复: 这里只会检查一下接收操作或发送操作是否可以进行(是否不会被阻塞)。有兴趣的话可以看一下 runtime/select.go 中的 selectgo 函数的源码。”
    -----这里是不是表示,对于那些符合条件但没有执行到的case,之前判断的时候是不是并没有从chan中取出数据?

    作者回复: 不好意思,问问题的人太多了,你问的时候最好带上比较完整的上下文。我下面按我目前对你问题的理解回答你吧。

    select 语句对某个 case 中的通道接收表达式的实际执行需要两个前提条件:

    1. 接收操作不会被阻塞。
    2. 当前 case 被选中。

    对于 case 中的通道发送表达式来说也是类似。因此,我们完全不用担心那些未被选中的通道操作会突然执行。


    2019-04-23
    1
  • Cxb
    @王小勃
    demo25 不输出second candidate 原因应该是channels[1]是非缓冲通道,select语句检测到阻塞状态,所以case语句不成立。把channels[1]设置为缓冲通道,或者写个协程接受channels[1]就可以输出second candidate
    func main() {
    //go func() {
    // fmt.Println(<-channels[1])
    //}()
    select {
    case getChan(0) <- getNumber(0):
    fmt.Println("The first candidate case is selected.")
    case getChan(1) <- getNumber(1):
    fmt.Println("The second candidate case is selected.")
    case getChan(2) <- getNumber(2):
    fmt.Println("The third candidate case is selected")
    default:
    fmt.Println("No candidate case is selected!")
    }
    }
    2019-03-07
    1
  • 一只傻哈皮
    请问select伪随机执行的目的是什么呢?没太理解这样做的目的。

    作者回复: 这属于语言特性之一,没有什么特殊原因。你可以理解为避免case书写顺序影响到执行顺序。

    2019-02-22
    1
  • 虢国技匠
    有个问题不明:demo25中 select为什么选择的不是第二个case输出?
    第二个case实际上在往channels[1] <- 2 ,那为什么不输出:The second candidate case is selected.
    package main

    import "fmt"

    var channels = [3]chan int{
    nil,
    make(chan int),
    nil,
    }

    var numbers = []int{1, 2, 3}

    func main() {
    select {
    case getChan(0) <- getNumber(0):
    fmt.Println("The first candidate case is selected.")
    case getChan(1) <- getNumber(1):
    fmt.Println("The second candidate case is selected.")
    case getChan(2) <- getNumber(2):
    fmt.Println("The third candidate case is selected")
    default:
    fmt.Println("No candidate case is selected!")
    }
    }

    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]
    }
    2018-12-21
    1
  • 一步
    老师你上面的发送通道<-是代表,可以向通道中发送数据呢?那对于通道而言是不是就是,通道接收数据,叫做接收通道?

    作者回复: 只能向它发送数据的我简称为发送通道。

    2018-09-14
    1
收起评论
40
返回
顶部