Go语言之通道

简介: 上一篇我们讲的原子函数和互斥锁,都可以保证共享数据的读写。但是呢,它们还是有点复杂,而且影响性能。对此,Go又为我们提供了一种工具,这就是通道。所以在多个goroutine并发中,我们不仅可以通过原子函数和互斥锁保证对共享资源的安全访问,消除竞争的状态,还可以通过使用通道,在多个goroutine发送和接受共享的数据,达到数据同步的目的。

上一篇我们讲的原子函数和互斥锁,都可以保证共享数据的读写。但是呢,它们还是有点复杂,而且影响性能。对此,Go又为我们提供了一种工具,这就是通道


所以在多个goroutine并发中,我们不仅可以通过原子函数和互斥锁保证对共享资源的安全访问,消除竞争的状态,还可以通过使用通道,在多个goroutine发送和接受共享的数据,达到数据同步的目的。


通道,它有点像在两个routine之间架设的管道:一个goroutine可以往这个管道里塞数据,另外一个可以从这个管道里取数据。有点类似于我们说的队列。


声明一个通道很简单,我们使用chan关键字即可。除此之外,还要指定通道中发送和接收数据的类型,这样我们才能知道,要发送什么类型的数据给通道,也知道从这个通道里可以接收到什么类型的数据。


ch:=make(chan int)


通道类型和Map这些类型一样,可以使用内置的make函数声明初始化。这里我们初始化了一个chan int类型的通道,所以我们只能往这个通道里发送int类型的数据,当然接收也只能是int类型的数据。


我们知道,通道是用于在goroutine之间通信的,它具有发送和接收两个操作,而且这两个操作的运算符都是<-


ch <- 2 //发送数值2给这个通道
x:=<-ch //从通道里读取值,并把读取的值赋值给x变量
<-ch //从通道里读取值,然后忽略


看例子,慢慢理解发送和接收的用法。发送操作<-在通道的后面,看箭头方向,表示把数值 2 发送到通道ch里;接收操作<-在通道的前面,而且是一个一元操作符,看箭头方向,表示从通道ch里读取数据。读取的数据可以赋值给一个变量,也可以忽略。


通道我们还可以使用内置的close函数关闭。


close(ch)


如果一个通道被关闭了,我们就不能往这个通道里发送数据了,如果发送的话,会引起painc异常。但是,我们还可以接收通道里的数据,如果通道里没有数据的话,接收的数据是nil


刚刚我们使用make函数初始化的时候,只有一个参数,其实make还可以有第二个参数,用于指定通道的大小。默认没有第二个参数的时候,通道的大小为 0 ,这种通道也被成为无缓冲通道


ch:=make(chan int)
ch:=make(chan int,0)
ch:=make(chan int,2)


看例子,其中第一个和第二个初始化是等价的。第三个初始化创建了一个大小为 2 的通道,这种称为有缓冲通道。


无缓冲的通道


无缓冲的通道指的是通道的大小为 0 。也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送goroutine和接收goroutine同时准备好,才可以完成发送和接收操作。


从上面无缓冲的通道定义来看,发送goroutine和接收gouroutine必须是同步的。同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为同步通道


func main() {
    ch := make(chan int)
    go func() {
            var sum int = 0
            for i := 0; i < 10; i++ {
                sum += i            }
            ch <- sum    }()

    fmt.Println(<-ch)}


在前面的例子中,我们为了演示goroutine,防止程序提前终止,都是使用sync.WaitGroup进行等待。现在的这个例子就不用了,我们使用同步通道来等待。


在计算sum和的goroutine没有执行完,把值赋给ch通道之前,fmt.Println(<-ch)会一直等待,所以main主goroutine就不会终止。只有当计算和的goroutine完成,并且发送到ch通道的操作准备好后,同时<-ch就会接收计算好的值,然后打印出来。


管道


我们在使用Bash的时候,有个管道操作|。它的意思是把上一个操作的输出,当成下一个操作的输入,连起来,做一连串的处理操作。


  ~ ls |grep 'D'
Desktop
Documents
Downloads


比如上面这个例子的意思是,先使用ls命令,把当前目录下的目录和文件列出来,作为下一个grep命令的输入,然后通过grep命令,匹配我们需要显示的目录和文件,这里匹配以D开头的文件名或者目录名。


其实我们使用通道也可以做到管道的效果,我们只需要把一个通道的输出,当成下一个通道的输入即可。


func main() {
    one := make(chan int)
    two := make(chan int)
    go func() {
        one<-100
    }()
    go func() {
        v:=<-one
        two<-v    }()

    fmt.Println(<-two)}


这里例子中我们定义两个通道onetwo,然后按照顺序,先把 100 发送给通道one,然后用另外一个goroutine从one接收值,再发送给通道two,最终在主goroutine里等着接收打印two通道里的值。这就类似于一个管道的操作,把通道one的输出,当成通道two的输入,类似于接力赛一样。


有缓冲的通道


有缓冲通道,其实是一个队列,这个队列的最大容量就是我们使用make函数创建通道时,通过第二个参数指定的。


ch := make(chan int, 3)


这里创建容量为 3 的、有缓冲的通道。对于有缓冲的通道,向其发送操作就是向队列的尾部插入元素,接收操作则是从队列的头部删除元素,并返回这个刚刚删除的元素。


当队列满的时候,发送操作会阻塞;当队列空的时候,接受操作会阻塞。有缓冲的通道,不要求发送和接收操作是同步的,相反可以解耦发送和接收操作。


想知道通道的容量以及里面有几个元素数据怎么办?其实和map一样,使用caplen函数就可以了。


cap(ch)
len(ch)


cap函数返回通道的最大容量,len函数返回现在通道里有几个元素


func mirroredQuery() string {
    responses := make(chan string, 3)
    go func() { responses <- request("asia.gopl.io") }()
    go func() { responses <- request("europe.gopl.io") }()    
    go func() { responses <- request("americas.gopl.io") }()    
    return <-responses // return the quickest response}func request(hostname string) (response string) { /* ... */ }


这是Go语言圣经里比较有意义的一个例子,例子是想获取服务端的一个数据,不过这个数据在三个镜像站点上都存在,这三个镜像分散在不同的地理位置,而我们的目的又是想最快地获取数据。


所以这里,我们定义了一个容量为 3 的通道responses,然后同时发起 3 个并发goroutine向这三个镜像获取数据,获取到的数据发送到通道responses中,最后我们使用return <-responses返回获取到的第一个数据,也就是最快返回的那个镜像的数据。


单向通道


有时候,我们有一些特殊场景,比如限制一个通道只可以接收,但是不能发送;有时候限制一个通道只能发送,但是不能接收,这种通道我们称为单向通道


定义单向通道也很简单,只需要在定义的时候,带上<-即可。


var send chan<- int //只能发送
var receive <-chan int //只能接收


注意<-操作符的位置,在后面是只能发送,对应发送操作;在前面是只能接收,对应接收操作。


单向通道应用于函数或者方法的参数比较多,比如:


func counter(out chan<- int) {
}


例子这样的,只能进行发送操作,防止使用接收操作。如果使用了接收操作,在编译的时候就会报错的。


使用通道可以很简单地在goroutine之间共享数据,下一篇会具体介绍一些例子,以便更好地理解并发。


目录
相关文章
|
30天前
|
存储 监控 算法
员工上网行为监控中的Go语言算法:布隆过滤器的应用
在信息化高速发展的时代,企业上网行为监管至关重要。布隆过滤器作为一种高效、节省空间的概率性数据结构,适用于大规模URL查询与匹配,是实现精准上网行为管理的理想选择。本文探讨了布隆过滤器的原理及其优缺点,并展示了如何使用Go语言实现该算法,以提升企业网络管理效率和安全性。尽管存在误报等局限性,但合理配置下,布隆过滤器为企业提供了经济有效的解决方案。
79 8
员工上网行为监控中的Go语言算法:布隆过滤器的应用
|
1月前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
46 7
|
1月前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
1月前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
113 71
|
1月前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
116 67
|
10天前
|
算法 安全 Go
Go语言中的加密和解密是如何实现的?
Go语言通过标准库中的`crypto`包提供丰富的加密和解密功能,包括对称加密(如AES)、非对称加密(如RSA、ECDSA)及散列函数(如SHA256)。`encoding/base64`包则用于Base64编码与解码。开发者可根据需求选择合适的算法和密钥,使用这些包进行加密操作。示例代码展示了如何使用`crypto/aes`包实现对称加密。加密和解密操作涉及敏感数据处理,需格外注意安全性。
33 14
|
1月前
|
Go 索引
go语言for遍历数组或切片
go语言for遍历数组或切片
116 62
|
10天前
|
Go 数据库
Go语言中的包(package)是如何组织的?
在Go语言中,包是代码组织和管理的基本单元,用于集合相关函数、类型和变量,便于复用和维护。包通过目录结构、文件命名、初始化函数(`init`)及导出规则来管理命名空间和依赖关系。合理的包组织能提高代码的可读性、可维护性和可复用性,减少耦合度。例如,`stringutils`包提供字符串处理函数,主程序导入使用这些函数,使代码结构清晰易懂。
50 11
|
10天前
|
存储 安全 Go
Go语言中的map数据结构是如何实现的?
Go 语言中的 `map` 是基于哈希表实现的键值对数据结构,支持快速查找、插入和删除操作。其原理涉及哈希函数、桶(Bucket)、动态扩容和哈希冲突处理等关键机制,平均时间复杂度为 O(1)。为了确保线程安全,Go 提供了 `sync.Map` 类型,通过分段锁实现并发访问的安全性。示例代码展示了如何使用自定义结构体和切片模拟 `map` 功能,以及如何使用 `sync.Map` 进行线程安全的操作。
|
15天前
|
监控 安全 算法
深度剖析核心科技:Go 语言赋能局域网管理监控软件进阶之旅
在局域网管理监控中,跳表作为一种高效的数据结构,能显著提升流量索引和查询效率。基于Go语言的跳表实现,通过随机化索引层生成、插入和搜索功能,在高并发场景下展现卓越性能。跳表将查询时间复杂度优化至O(log n),助力实时监控异常流量,保障网络安全与稳定。示例代码展示了其在实际应用中的精妙之处。
36 9