专栏首页学院君的专栏Go 语言并发编程系列(六)—— 通道类型篇:单向通道及其使用

Go 语言并发编程系列(六)—— 通道类型篇:单向通道及其使用

我们介绍了管道类型的基本语法,通常,管道都是支持双向操作的:既可以往管道发送数据,也可以从管道接收数据。但在某些场景下,可能我们需要限制只能往管道发送数据,或者只能从管道接收数据,这个时候,就需要用到单向通道。

不过,这里我们需要澄清一下,通道本身还是要支持读写的,如果某个通道只支持写入操作,那么即便数据写进去了,不能被读取也毫无意义,同理,如果某个通道只支持读取操作,不能写入数据,那么通道永远是空的,从一个空的通道读取数据会导致协程的阻塞,无法执行后续代码。

因此,Go 语言支持的单向管道,实际上是在使用层面对通道进行限制,而不是语法层面:即我们在某个协程中只能对通道进行写入操作,而在另一个协程中只能对该通道进行读取操作。从这个层面来说,单向通道的作用是约束在生产协程中只能发送数据到通道,而在消费协程中只能从通道接收数据,从而让代码遵循「最小权限原则」,避免误操作和通道使用的混乱,让代码更加稳健。

下面我们就来看看如何在 Go 协程之间实现单向通道的约束。

当我们将一个通道类型变量传递到一个函数时(通常是在另外一个协程中执行),如果这个函数只能发送数据到通道,可以通过如下将其指定为单向只写通道(发送通道):

func test(ch chan<- int)

上述代码限定在 test 函数中只能写入 int 类型数据到通道 ch

反过来,如果我们将一个通道类型变量传递到一个只允许从该通道读取数据的函数,可以通过如下方式将通道指定为单向只读通道(接收通道):

func test(ch <-chan int)

上述代码限定在 test 函数中只能从 ch 通道读取 int 类型数据。

虽然我们也可以像声明正常通道类型那样声明单向通道,但我们一般不这么做,因为这样一来,就是从语法上限定通道的操作类型了,对于只读通道只能接收数据,对于只写通道只能发送数据:

var ch1 chan int
var ch2 <-chan int 
var ch3 chan<- int

单向通道的初始化和双向通道一样:

ch1 := make(chan int)
ch2 := make(<-chan int)
ch3 := make(chan<- int)

此外,我们还可以通过如下方式实现双向通道和单向通道的转化:

ch1 := make(chan int) 
ch2 := <-chan int(ch1)
ch3 := chan<- int(ch1)

基于双向通道 ch1,我们通过类型转化初始化了两个单向通道:单向只读的 ch2 和单向只写的 ch3。注意这个转化是不可逆的,双向通道可以转化为任意类型的单向通道,但单向通道不能转化为双向通道,读写通道之间也不能相互转化。

实际上,我们在将双向通道传递到限定通道参数操作类型的函数时,就应用到了类型转化。

我们可以通过单向通道来约束上篇教程的示例代码中子协程对通道的单向写入操作:

package main

import (
    "fmt"
    "time"
)

func test(ch chan<- int) {
    for i := 0; i < 100; i++ {
        ch <- i
    }
    close(ch)
}

func main() {
    start := time.Now()
    ch := make(chan int, 20)
    go test(ch)
    for i := range ch {
        fmt.Println("接收到的数据:", i)
    }
    end := time.Now()
    consume := end.Sub(start).Seconds()
    fmt.Println("程序执行耗时(s):", consume)
}

如果我们将 test 函数中的通道参数类型约束调整为 test(ch <-chan int),编译代码就会报错:

# command-line-arguments
./channel3.go:10:12: invalid operation: ch <- i (send to receive-only type <-chan int)
./channel3.go:12:10: invalid operation: close(ch) (cannot close receive-only channel)

提示传入的通道是只读通道(receive-only channel),不能进行写入操作,此外,关闭通道函数 close 也不能作用到只读通道。

如果将 main 函数中的通道初始化语句修改为 ch := make(chan<- int),编译时也会报错:

# command-line-arguments
./channel3.go:19:14: invalid operation: range ch (receive from send-only type chan<- int)

提示不能通过 range 语句从只写通道(send-only)中接收数据。

我们也可以定义一个返回值类型为单向只写通道的函数,以便得到该返回值的代码只能从通道中接收数据:

func test() <-chan int {
    ch := make(chan int, 20)
    for i := 0; i < 100; i++ {
        ch <- i
    }
    close(ch)
    return ch
}

显然,合理使用单向通道,可以有效约束不同业务对通道的操作,避免越权使用和滥用,此外,也提高了代码的可读性,一看函数参数就可以判断出业务对通道的操作类型。

本文分享自微信公众号 - 学院君的后花园(geekacademy),作者:学院君

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Go 语言并发编程系列(五)—— 通道类型篇:基本语法和缓冲通道

    在上篇教程中,学院君给大家演示了如何通过通道(channel)传递消息实现 Go 协程间的通信, 接下来,我们将通过几篇教程的篇幅来系统了解通道类型及其使用,从...

    学院君
  • Go 语言并发编程系列(四)—— 协程通信实现之消息传递篇

    上篇教程学院君演示了如何通过共享内存实现协程通信,不过这种方式太过繁琐,且维护成本高,Go 语言推荐使用消息传递实现并发通信,这种消息通信机制被称为 chann...

    学院君
  • Go 语言并发编程系列(八)—— 通道类型篇:错误和异常处理

    在前面几篇通道教程中,我们陆续介绍了与通道相关的基本语法、单向通道以及 select 语句,有关通道的基本知识就介绍到这里,今天我们来看下通道使用过程中的错误和...

    学院君
  • 面试官:手撕十大排序算法,你会几种?

    2020年7月24日,阴,气温15摄氏度,已经两天没有涨粉丝了,一个人运营公众号确实有些吃力。尽管这样,也不影响我前进的脚步,搬砖的路上,我们一起加油!!!

    C you again
  • 2015年javaB组1-4题解析与理解

    X老板脾气古怪,他们公司的电话分机号都是3位数,老板规定,所有号码必须是降序排列,且不能有重复的数位。比如:

    萌萌哒的瓤瓤
  • 动画+原理+代码,解读十大经典排序算法

    排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见...

    昱良
  • 十大经典排序算法

    排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见...

    用户1257393
  • 十大经典排序算法(动态演示+代码)

    堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大...

    用户6543014
  • 十大经典排序算法(代码实现),建议收藏

    兜兜转转,一晃年关将至。时间证明了一个道理,学啥忘啥,学的越快忘得越快,还不如踏踏实实写点笔记心得来的实在。

    DeROy
  • Java 必会10大的经典算法

    原文链接:https://github.com/hustcc/JS-Sorting-Algorithm

    业余草

扫码关注云+社区

领取腾讯云代金券