如何使用Go频道
Go通道是一种通信机制,允许Goroutines交换数据。当开发者有许多Goroutines同时运行时,通道是最方便的相互通信方式。
开发人员经常使用这些通道来通知和管理应用程序的并发性。
在这篇文章中,我们将介绍Go通道的一般用途,包括如何向通道中写入和从通道中读出,如何使用通道作为函数参数,以及如何使用范围来迭代它们。
创建一个Go通道结构
首先,让我们使用make
函数在Go中创建一个通道。
// for example if channel created using following :
ch := make(chan string)
// this is the basic structure of channels
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // pointer to an array of dataqsiz elements
elementSize uint16
closed uint32
sendx uint // send index
recvx uint // receive index
recvq waitq // list of receive queue
sendq waitq // list of send queue
lock mutex // lock protects all fields in hchan, as well as several
}
Go通道的用途
在本节中,我们将回顾Go通道的用途,以及它们如何有利于应用开发。
将Go通道作为期货和承诺使用
开发人员经常在Go中使用期货和承诺来处理请求和响应。例如,如果我们想实现async/await模式,我们必须添加以下内容。
package main
import (
"fmt"
"math/rand"
"time"
)
func longTimedOperation() <-chan int32 {
ch := make(chan int32)
func run(){
defer close(ch)
time.Sleep(time.Second * 5)
ch <- rand.Int31n(300)
}
go run()
return ch
}
func main(){
ch := longTimedOperation()
fmt.Println(ch)
}
通过简单地使用5秒的延迟来模拟一个长期运行的进程,我们可以向一个通道发送一个随机的整数值,等待该值,并接收它。
使用Go通道进行通知
通知是独一无二的请求或返回值的响应。我们通常使用空白结构类型作为通知通道的元素类型,因为空白结构类型的大小为零,意味着结构的值不会消耗内存。
例如,用通道实现一对一的通知会收到一个通知值。
package main
import (
"fmt"
"time"
)
type T = struct{}
func main() {
completed := make(chan T)
go func() {
fmt.Println("ping")
time.Sleep(time.Second * 5) // heavy process simulation
<- completed // receive a value from completed channel
}
completed <- struct{}{} // blocked waiting for a notification
fmt.Println("pong")
}
这让我们可以使用从一个通道收到的值来提醒另一个等待向同一通道提交值的Goroutine。
渠道也可以安排通知。
package main
import (
"fmt"
"time"
)
func scheduledNotification(t time.Duration) <- chan struct{} {
ch := make(chan struct{}, 1)
go func() {
time.Sleep(t)
ch <- struct{}{}
}()
return ch
}
func main() {
fmt.Println("send first")
<- scheduledNotification(time.Second)
fmt.Println("secondly send")
<- scheduledNotification(time.Second)
fmt.Println("lastly send")
}
使用Go通道作为计数信号系统
为了规定最大的并发请求数,开发者经常使用计数信号器来锁定和解锁并发进程,以控制资源和应用相互排斥。例如,开发者可以控制数据库中的读写操作。
有两种方法可以获得一块信道信号的所有权,类似于把信道作为突变器使用。
- 通过发送获取所有权,并通过接收释放
- 通过接收获得所有权并通过发送释放
然而,当拥有一个通道信号时,有一些特殊的规则。首先,每个通道允许交换特定的数据类型,这也被称为通道的元素类型。
第二,为了使通道正常运行,必须有人接收通过通道发送的东西。
例如,我们可以使用chan
关键字声明一个新的通道,我们可以使用close()
函数关闭一个通道。因此,如果我们使用< -
通道语法来阻止代码从通道中读取,一旦完成,我们就可以关闭它。
最后,当使用一个通道作为一个函数参数时,我们可以指定它的方向,也就是说指定通道是用于发送还是接收。
如果我们事先知道一个通道的用途,就使用这种能力,因为它使程序更健壮,更安全。这意味着我们不能意外地将数据发送到一个只接收数据的通道,或从一个只发送数据的通道接收数据。
因此,如果我们声明一个通道函数参数将只用于读,而我们试图向它写,我们会得到一个错误信息,这很可能使我们免于讨厌的bug。
写入一个Go通道
本小节的代码教我们如何在Go中向一个通道写入。将值x
写到通道c
,就像写c <- x
一样简单。
箭头显示了值的方向;只要x
和c
有相同的类型,我们就不会有问题。
在下面的代码中,chan
关键字声明了c
函数参数是一个通道,并且后面必须跟上通道的类型,也就是int
。然后,c <- x
语句允许我们将值x
写到通道c
,close()
函数关闭了该通道。
package main
import (
"fmt"
"time"
)
func writeToChannel(c chan int, x int) {
fmt.Println(x)
c <- x
close(c)
fmt.Println(x)
}
func main() {
c := make(chan int)
go writeToChannel(c, 10)
time.Sleep(1 * time.Second)
}
最后,执行前面的代码会产生下面的输出。
$ go run writeCh.go
10
这里奇怪的是,writeToChannel()
函数只打印了一次给定的值,这是在第二个fmt.Println(x)
语句从未执行时造成的。
原因很简单:c <- x
语句阻止了writeToChannel()
函数其余部分的执行,因为没有人在读取写入c
通道的内容。
因此,当time.Sleep(1 * time.Second)
语句完成后,程序就终止了,不需要等待writeToChannel()
。
下一节说明了如何从一个通道中读取数据。
从一个Go通道中读取数据
我们可以通过执行<-c
,从一个名为c
的通道中读取一个单值。在这种情况下,方向是从通道到外部范围。
package main
import (
"fmt"
"time"
)
func writeToChannel(c chan int, x int) {
fmt.Println("1", x)
c <- x
close(c)
fmt.Println("2", x)
}
func main() {
c := make(chan int)
go writeToChannel(c, 10)
time.Sleep(1 * time.Second)
fmt.Println("Read:", <-c)
time.Sleep(1 * time.Second)
_, ok := <-c
if ok {
fmt.Println("Channel is open!")
} else {
fmt.Println("Channel is closed!")
}
}
writeToChannel()
函数的实现与之前的相同。在前面的代码中,我们使用<-c
符号从通道c
读取。
第二个time.Sleep(1 * time.Second)
语句给了我们从通道中读取的时间。
当通道关闭时,当前的Go代码可以正常工作;但是,如果通道是开放的,这里介绍的Go代码会丢弃通道的读取值,因为我们在_, ok := <-c
语句中使用了_
字符。
如果我们还想在通道打开的情况下存储在通道中发现的值,请使用适当的变量名而不是_
。
执行readCh.go
,会产生以下输出。
$ go run readCh.go
1 10
Read: 10
2 10
Channel is closed!
$ go run readCh.go
1 10
2 10
Read: 10
Channel is closed!
尽管输出仍然是不确定的,但writeToChannel()
函数的两个fmt.Println(x)
语句都执行了,因为当我们从通道中读取时,通道解锁了。
从一个封闭的通道接收信息
在本小节中,我们将回顾一下当我们试图使用readClose.go
中的Go代码从一个封闭的通道中读出时会发生什么。
在readClose.go
程序的这一部分,我们必须创建一个新的int
通道,命名为willClose
,向其写入数据,读取数据,并在接收数据后关闭该通道。
package main
import (
"fmt"
)
func main() {
willClose := make(chan int, 10)
willClose <- -1
willClose <- 0
willClose <- 2
<-willClose
<-willClose
<-willClose
close(willClose)
read := <-willClose
fmt.Println(read)
}
执行前面的代码(保存在readClose.go
文件中)会产生以下输出。
$ go run readClose.go
0
这意味着从一个关闭的通道读取数据会返回其数据类型的零值,在这种情况下是0
。
作为函数参数的通道
虽然我们在处理readCh.go
或writeCh.go
时没有使用函数参数,但Go允许我们在把一个通道作为函数参数时指定它的方向,即它是用于读还是写。
这两种类型的通道被称为单向通道,而通道默认是双向的。
检查一下下面两个函数的Go代码。
func f1(c chan int, x int) {
fmt.Println(x)
c <- x
}
func f2(c chan<- int, x int) {
fmt.Println(x)
c <- x
}
虽然这两个函数实现了相同的功能,但它们的定义却略有不同。这种差异是由f2()
函数定义中chan
关键字右边的<-
符号造成的。
这表示c
通道只能写入。如果Go函数的代码试图从一个只能写的通道(也被称为只能送的通道)参数中读取,Go编译器会产生以下错误信息。
# command-line-arguments
a.go:19:11: invalid operation: range in (receive from send-only type chan<-int)
同样地,我们可以有以下的函数定义。
func f1(out chan<- int64, in <-chan int64) {
fmt.Println(x)
c <- x
}
func f2(out chan int64, in chan int64) {
fmt.Println(x)
c <- x
}
f2()
的定义结合了一个名为in的只读通道和一个名为out的只写通道。如果我们不小心试图写入并关闭一个函数的只读通道(也被称为只接收通道)参数,我们会得到以下错误信息。
# command-line-arguments
a.go:13:7: invalid operation: out <- i (send to receive-only type <-chan int)
a.go:15:7: invalid operation: close(out) (cannot close receive-only
channel)
在Go通道上的范围
我们可以在Golang中使用范围语法来迭代一个通道以读取其值。这里的迭代应用了先进先出(FIFO)的概念:只要我们向通道缓冲区添加数据,我们就可以像队列一样从缓冲区读取数据。
package main
import "fmt"
func main() {
ch := make(chan string, 2)
ch <- "one"
ch <- "two"
close(ch)
for elem := range ch {
fmt.Println(elem)
}
}
如上所述,使用范围从通道迭代应用了先进先出原则(从队列中读取)。所以,执行前面的代码会输出以下结果。
$ go run range-over-channels.go
one
two
结论
Go通道用于在同时运行的函数之间通过发送和接收特定元素类型的数据进行通信。当我们有许多Goroutine同时运行时,通道是它们之间最方便的通信方式。
谢谢你的阅读,祝你编码愉快
The postHow to use Go channelsappeared first onLogRocket Blog.
转载自:https://juejin.cn/post/7068582089674719262