Golang 中的 Channel 简介

文章目录

这篇文章重点介绍了通道(Channel)在 Go 中的工作方式,以及如何在代码中使用它们。

在 Go 中,Channel 是一种编程结构,它允许我们在代码的不同部分之间传输数据,通常来自不同的 goroutine。

创建通道

我们可以通过使用 chan 关键字和数据类型来声明一个新的通道类型:

var c chan int

在这里,cchan int 类型——这意味着它是一个发送 int 类型的通道。 channel 的默认值为 nil,所以我们需要在使用前先赋值。

或者,我们可以使用 make 函数来声明和初始化通道:

c := make(chan int)

发送和接收数据

现在让我们看一些使用通道获取乘法结果的代码(示例1):

package main

import (
	"fmt"
)

func main() {
	n := 3

    // 我们在这里“make”了一个channel
    // 可以用来传输 `int` 数据类型
	out := make(chan int)

    // 我们将这个函数作为一个 goroutine 来运行,
    // 使用上面的 channel 作为参数
	go multiplyByTwo(n, out)

    // 一旦在该通道上接收到任何输出,将其打印到控制台并继续
	fmt.Println(<-out)
}

// 这个函数现在接受一个通道作为它的第二个参数
func multiplyByTwo(num int, out chan<- int) {
	result := num * 2

    // 将结果输出到Channel中
	out <- result
}

你可以在这里执行这段代码

Channel 为我们提供了一种“连接”我们程序的不同并发部分的方法。它可以被认为是连接我们代码的不同并发部分的“管道”或“动脉”。

定向通道

Channel 可以是定向的——这意味着您可以将通道限制为仅发送或仅接收数据。这由 channel 声明附带的箭头 (<-) 指定

例如,看一下 multiplyByTwo 函数的 out 参数的类型定义:

out chan<- int
  • chan<- 声明告诉我们您只能将数据发送到通道中,但不能从中接收数据。
  • int 声明告诉我们 channel 将只接受 int 数据类型。

虽然它们看起来像独立的部分,但 chan<- int 可以被认为是一种数据类型,它描述了一个**“仅发送”的整数通道** 。

同样,“仅接收”通道 声明的示例如下所示:

out <-chan int

您还可以在不提供方向性的情况下声明一个通道,这意味着它可以发送或接收数据:

out chan int

当我们在 main 函数中创建 out 通道时可以看到这一点:

out := make(chan int)

然后可以根据您要在代码中其他地方施加的限制,将此通道转换为定向通道。

阻塞条件

从通道发送或接收值的语句会在它们自己的 goroutine 中进行阻塞。也就是说:

  • 从 channel 接收数据的语句将会阻塞,直到接收到一些数据
  • 向 channel 发送数据的语句将等待,直到发送的数据被接收

例如,在 main 函数中,当我们尝试打印接收到的值时:

fmt.Println(<-out)

<-out 语句将阻塞代码,直到在 out 通道上接收到数据。

注意:从 nil channel 发送或接收数据将永远阻塞。

创建 Worker

示例1可以用另一种方式实现,使用 2 个通道:一个用于向 goroutine 发送数据,另一个用于接收结果(运行此代码)。

func main() {
	n := 3
	in := make(chan int)
	out := make(chan int)

	// 我们现在为 `multiplyByTwo` 函数提供 2 个通道
    // 一个用于发送数据,一个用于接收
	go multiplyByTwo(in, out)

	// 然后我们通过通道向它发送数据并等待结果
	in <- n
	fmt.Println(<-out)
}

func multiplyByTwo(in <-chan int, out chan<- int) {
	fmt.Println("Initializing goroutine...")

    // 在“in”通道上接收到数据之前,goroutine 不会继续,会阻塞在此处
	num := <-in

	// 其余逻辑和之前的一样
	result := num * 2
	out <- result
}

将工作分配给 goroutines 的一种常见模式是生成持久 Worker 并通过通道发送和接收信息。

func main() {
	out := make(chan int)
	in := make(chan int)

	// 创建 3 个 `multiplyByTwo` worker
	go multiplyByTwo(in, out)
	go multiplyByTwo(in, out)
	go multiplyByTwo(in, out)

    // 到目前为止,创建的 goroutines 都没有真正做任何事,
    // 因为它们都在等待 `in` 通道接收数据
    // 现在我们在另一个 goroutine 中发送数据到 in 通道
	go func() {
		in <- 1
		in <- 2
		in <- 3
		in <- 4
	}()

	// 现在我们等待每个结果进入out通道并打印
	fmt.Println(<-out)
	fmt.Println(<-out)
	fmt.Println(<-out)
	fmt.Println(<-out)
}

func multiplyByTwo(in <-chan int, out chan<- int) {
	fmt.Println("Initializing goroutine...")
	for {
		num := <-in
		result := num * 2
		out <- result
	}
}

产生的 worker 数对应于您想要的并发数。

在上面的例子中,我们产生了三个 worker,有四个任务。前三个任务会立即分配一个 worker,但第四个任务必须等其中一个worker完成后才能被执行。

“Select”声明

当有多个通道等待接收信息,并且希望在其中任何一个通道完成时执行某个操作,可以使用 select 语句。

select {
case res := <-channel1:
	// do something
case channel2 <- someData:
	// do something else
case <- channel3:
	// do another thing
}

在这里,执行的操作取决于哪个情况先完成-其他情况将被忽略。

让我们看一个例子,我们有一个快速的 worker 和一个慢速的 worker 来执行乘法:

// `fast` 和 `slow` 函数做同样的事情
// 但 `slow` 需要更多时间完成
func fast(num int, out chan<- int) {
	result := num * 2
	time.Sleep(5 * time.Millisecond)
	out <- result

}

func slow(num int, out chan<- int) {
	result := num * 2
	time.Sleep(15 * time.Millisecond)
	out <- result
}

func main() {
	out1 := make(chan int)
	out2 := make(chan int)

    // 我们使用不同的goroutine 和不同的 channel 来执行 fast 和 slow
	go fast(2, out1)
	go slow(3, out2)

    // 执行哪个操作是看哪个 channel 先接收到信息
	select {
	case res := <-out1:
		fmt.Println("fast finished first, result:", res)
	case res := <-out2:
		fmt.Println("slow finished first, result:", res)
	}
}

如果我们运行这段代码,我们将得到输出:

fast finished first, result: 4

select 语句由 out1 触发并忽略 out2 中指定的操作。

select 语句的一个常见用例是结合 context 检测何时需要取消某个操作——如果我们正在执行一个时间敏感的操作,我们理想情况下希望有一个执行的截止时间,在操作停滞或花费太长时间时可以快速失败。

缓冲通道

在前面的几个例子中,我们看到通道语句阻塞,直到数据被发送到通道或从通道接收。

发生这种情况是因为通道没有任何地方可以“存储”进入它的数据,因此需要等待语句来接收数据。

缓冲通道是一种内部具有存储容量的通道。要创建缓冲通道,我们向 make 语句添加第二个参数以指定容量:

out := make(chan int, 3)

现在 out 是一个容量为 3 个整数变量的缓冲通道。这意味着它在阻塞之前最多可以接受 3 个值:

package main

import "fmt"

func main() {
	out := make(chan int, 3)
	out <- 1
	out <- 2
	out <- 3

    // 这个语句会阻塞
	out <- 4
}

您可以将缓冲通道视为普通通道加上存储(或缓冲区)。

缓冲通道用于在没有可用接收者时,我们不希望通道语句被阻塞的情况。添加缓冲区可以让我们等待一些接收者被释放,而不会阻塞发送代码。

为什么我们需要 Channel?

通道允许我们在并发运行的代码的不同部分之间进行惯用通信。

由于通道语句在发送或接收数据时会阻塞,因此它使我们的代码不易出错,因为许多错误都是由我们在写入值之前读取值的数据竞争引起的。

Channel 使 Go 中的并发编程变得更加容易,并使您的代码在某些情况下更具可读性。

并不是所有的地方都需要使用 channel。有时,使用指针和 waitgroup 来传递信息可能更加容易。

与所有并发编程一样,避免竞争条件很重要,因为这会产生难以预测的错误。


也可以看看


全国大流量卡免费领

19元月租ㆍ超值优惠ㆍ长期套餐ㆍ免费包邮ㆍ官方正品