跳转到主要内容

在本教程中,我们将研究如何在基于 Go 的应用程序中使用通道。

通道是连接基于 Go 的应用程序中的 goroutine 之间的管道,允许通信以及随后在变量之间传递值。

与其他编程语言相比,它们非常方便,可以帮助您在 Go 中构建令人难以置信的高性能、高并发应用程序,而无需大惊小怪。这绝不是侥幸,在设计语言时,核心开发人员决定他们希望他们的语言中的并发性成为一等公民,并使其尽可能简单地使用,不要走得太远,也不允许开发人员他们需要工作的自由。

如此轻松地构建并发系统的能力首先吸引了我对这门语言的兴趣,我不得不说,到目前为止,这绝对是一种乐趣。

注意 - 如果您想了解有关 goroutine 的更多信息,我建议您查看我的其他关于 goroutines 的教程。

目标


在本教程结束时,您将:

  • 对渠道背后的理论有深刻的理解
  • 能够创建使用通道的简单并发 Go 应用程序

先决条件


为了完成本教程,您需要满足以下先决条件:

  • 你需要在你的机器上安装 Go。

视频教程


如果您愿意,本教程以视频格式提供。

https://youtu.be/e4bu9g-bYtg

理论


通道的概念并不是什么新鲜事物,就像 Go 的许多并发特性一样,这些概念是从 Hoare 的 Communicating Sequential Processes (1978),简称 CSP,甚至是 Dijkstra 的受保护命令( 1975 年)。

然而,Go 的开发人员的使命是以尽可能简单的方式呈现这些概念,以使程序员能够创建更好、更正确、高度并发的应用程序。

一个简单的例子


让我们从看看如何构建一个非常简单的例子开始,看看它是如何在 Go 中工作的。 我们将首先创建一个函数,它会计算一个任意的随机值,并将其传递回一个名为 values 的通道变量:

main.go

package main

import (
    "fmt"
    "math/rand"
)

func CalculateValue(values chan int) {
    value := rand.Intn(10)
    fmt.Println("Calculated Random Value: {}", value)
    values <- value
}

func main() {
    fmt.Println("Go Channel Tutorial")

  values := make(chan int)
  defer close(values)

    go CalculateValue(values)

    value := <-values
    fmt.Println(value)
}

让我们剖析一下这里发生了什么。在我们的 main() 函数中,我们调用了 values := make(chan int),这个调用有效地创建了我们的新通道,以便我们随后可以在我们的 CalculateValue goroutine 中使用它。

注意 - 我们在实例化我们的值通道时使用了 make,因为像地图和切片一样,通道必须在使用前创建。

在我们创建了通道之后,我们调用了 defer close(values),它将通道的关闭推迟到 main() 函数执行结束。这通常被认为是确保我们自己整理的最佳实践。

在我们调用 defer 之后,我们继续启动我们的单个 goroutine:CalculateValue(values) 传入我们新创建的 values 通道作为其参数。在我们的 CalculateValue 函数中,我们计算一个介于 1-10 之间的单个随机值,将其打印出来,然后通过调用 values <- value 将该值发送到我们的值通道。

回到我们的 main() 函数,然后我们调用 value := <-values 从我们的 values 通道接收一个值。

注意 - 注意当我们执行这个程序时,它不会立即终止。这是因为向通道发送和从通道接收的行为是阻塞的。我们的 main() 函数会阻塞,直到它从我们的通道接收到一个值。

执行此代码后,您应该会看到输出如下所示:

$ go run main.go
Go Channel Tutorial
Calculated Random Value: {} 7
7

Summary:

myChannel := make(chan int) - creates myChannel which is a channel of type int

channel <- value - sends a value to a channel

value := <- channel - receives a value from a channel

 

所以,到目前为止,在你的 Go 程序中实例化和使用通道看起来相当简单,但是在更复杂的场景中呢?

无缓冲通道


在你的 goroutine 中使用传统通道有时会导致你可能不太期待的行为问题。对于传统的无缓冲通道,每当一个 goroutine 向该通道发送一个值时,该 goroutine 将随后阻塞,直到从该通道接收到该值。

让我们在一个真实的例子中看到这一点。如果我们看一下下面的代码,它与我们之前的代码非常相似。但是,我们扩展了 CalculateValue() 函数以在将随机计算的值发送到通道后执行 fmt.Println。

在我们的 main() 函数中,我们添加了对 goCalculateValue(valueChannel) 的第二次调用,因此我们应该期望有 2 个值会快速连续地发送到该通道。

main.go

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func CalculateValue(c chan int) {
    value := rand.Intn(10)
    fmt.Println("Calculated Random Value: {}", value)
    time.Sleep(1000 * time.Millisecond)
    c <- value
    fmt.Println("Only Executes after another goroutine performs a receive on the channel")
}

func main() {
    fmt.Println("Go Channel Tutorial")

    valueChannel := make(chan int)
    defer close(valueChannel)

    go CalculateValue(valueChannel)
    go CalculateValue(valueChannel)

    values := <-valueChannel
    fmt.Println(values)
}

但是,当你运行它时,你应该会看到只有我们的第一个 goroutine 的最终打印语句实际上被执行了:

 

go run main.go
Go Channel Tutorial
Calculated Random Value: {} 1
Calculated Random Value: {} 7
1
Only Executes after another goroutine performs a receive on the channel

 


原因是我们对 c <- value 的调用在我们的第二个 goroutine 中被阻塞,随后 main() 函数在我们的第二个 goroutine 有机会完成自己的执行之前结束它的执行。

缓冲通道


绕过这种阻塞行为的方法是使用一种叫做缓冲通道的东西。这些缓冲通道本质上是给定大小的队列,可用于跨 goroutine 通信。为了创建缓冲通道而不是无缓冲通道,我们为 make 命令提供了容量参数:

bufferedChannel := make(chan int, 3)
通过将其更改为缓冲通道,如果通道已满,我们的发送操作 c <- value 只会在我们的 goroutine 中阻塞。

让我们修改现有程序以使用缓冲通道并查看输出。请注意,我在 main() 函数的底部添加了对 time.Sleep() 的调用,以便延迟阻塞我们的 main() 函数以允许我们的 goroutine 完成执行。

main.go

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func CalculateValue(c chan int) {
    value := rand.Intn(10)
    fmt.Println("Calculated Random Value: {}", value)
    time.Sleep(1000 * time.Millisecond)
    c <- value
    fmt.Println("This executes regardless as the send is now non-blocking")
}

func main() {
    fmt.Println("Go Channel Tutorial")

    valueChannel := make(chan int, 2)
    defer close(valueChannel)

    go CalculateValue(valueChannel)
    go CalculateValue(valueChannel)

    values := <-valueChannel
    fmt.Println(values)

    time.Sleep(1000 * time.Millisecond)
}

现在,当我们执行这个时,我们应该看到我们的第二个 goroutine 确实继续执行它,而不管在我们的 main() 函数中没有调用第二个接收的事实。感谢 time.Sleep(),我们可以清楚地看到无缓冲通道及其阻塞特性与缓冲通道及其非阻塞(未满时)特性之间的区别。

 

Go Channel Tutorial
Calculated Random Value: {} 1
Calculated Random Value: {} 7
7
This executes regardless as the send is now non-blocking
This executes regardless as the send is now non-blocking

 


结论


因此,在这个相当长的教程中,我们设法了解了 Go 中各种不同类型的通道。我们发现了缓冲通道和无缓冲通道之间的差异,以及我们如何在并发 go 程序中利用它们来发挥我们的优势。

如果您喜欢本教程,请随时在下面的评论部分告诉我。如果您对我可以做得更好有任何建议,那么我很乐意在下面的评论部分中听到它们!

延伸阅读


如果您喜欢这篇文章并希望了解有关在 Go 中使用并发的更多信息,那么我建议您查看我们关于并发的其他文章:

文章链接