百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Go (Golang) 中的 Channels 简介(golang channel长度和容量)

bigegpt 2025-07-10 13:19 11 浏览

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

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

为什么我们需要Channels?

要理解通道,我们必须首先知道如何可视化 goroutine。

让我们从一个简单的 goroutine 开始,它接受一个数字,将它乘以 2,然后打印它的值:

package main

import (
	"fmt"
	"time"
)

func main() {
	n := 3

	// We want to run a goroutine to multiply n by 2
	go multiplyByTwo(n)

	// We pause the program so that the `multiplyByTwo` goroutine
	// can finish and print the output before the code exits
	time.Sleep(time.Second)
}

func multiplyByTwo(num int) int {
	result := num * 2
	fmt.Println(result)
	return result
}

我们可以将这个程序可视化为一组两个块:一个是main主函数,另一个是multiplyByTwo goroutine。

这个实现的问题是我们代码的这两个部分是相当不连贯的。作为结果 :

  • 我们的主函数(main)无法访问函数multiplyByTwo中的结果。
  • 我们无法知道multiplyByTwogoroutine 何时完成。因此,我们必须main通过调用来暂停函数time.Sleep,这种实现方式是不太好的。

创建Channels

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

var c chan int

在这里,c 是发送类型chan int的通道。int通道的默认值为nil,所以我们需要赋值

在 Goroutine 之间添加Channels

现在让我们看一些使用通道来获取乘法结果的代码:

package main

import (
	"fmt"
)

func main() {
	n := 3

	// This is where we "make" the channel, which can be used
	// to move the `int` datatype
	out := make(chan int)

	// We still run this function as a goroutine, but this time,
	// the channel that we made is also provided
	go multiplyByTwo(n, out)

	// Once any output is received on this channel, print it to the console and proceed
	fmt.Println(<-out)
}

// This function now accepts a channel as its second argument...
func multiplyByTwo(num int, out chan<- int) {
	result := num * 2

	//... and pipes the result into it
	out <- result
}

通道为我们提供了一种“连接”程序不同并发部分的方法。在这种情况下,我们可以直观地表示两个并发代码块之间的这种联系:

此处的绿色箭头表示数据通过通道

通道可以被认为是连接我们代码的不同并发部分的“管道”或“动脉”。

定向通道(Directional Channels)

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

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

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

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

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

out <-chan int

您还可以声明一个通道而不给出方向性,这意味着它可以发送或接收数据:

out chan int

就像我们在main函数中创建out通道:

out := make(chan int)

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

阻塞语句

从通道发送或接收值的语句在它们自己的 goroutine 中是阻塞的。这表示:

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

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

fmt.Println(<-out)

该<-out语句将阻塞代码,直到out在通道上接收到一些数据。然后通过将块分成两部分来帮助可视化这一点main:运行直到等待通道接收数据的部分,以及之后运行的部分。

这里添加的虚线箭头是为了表明它是启动goroutine的main函数。multiplyByTwo

的第二部分main只能在通过通道接收到数据后运行(由绿色箭头描绘)

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

通道工作者(Channel Workers)

Example #1 可以用另一种方式实现,使用 2 个通道:一个用于向 goroutine 发送数据,另一个用于接收结果。

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

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

	// Create 3 `multiplyByTwo` goroutines.
	go multiplyByTwo(in, out)
	go multiplyByTwo(in, out)
	go multiplyByTwo(in, out)

	// Up till this point, none of the created goroutines actually do
	// anything, since they are all waiting for the `in` channel to
	// receive some data, we can send this in another goroutine
	go func() {
		in <- 1
		in <- 2
		in <- 3
		in <- 4
	}()

	// Now we wait for each result to come in
	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
	}
}

现在,除了main,multiplyByTwo也可以分成2部分:我们在in通道上等待的点之前和之后的部分(num := <- in)

产生的worker数对应于并发进程数。

在上面的例子中,我们产生了三个worker,并且有四个任务。前三个任务将立即分配一个worker,但第四个任务必须等到其中一个worker完成。

即使task4准备好了,它也需要等到至少一个worker空闲。

“Select”声明

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

select {
case res := <-someChannel:
	// do something
case anotherChannel <- someData:
	// do something else
case <- yetAnotherChannel:
	// do another thing
}

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

让我们看一个例子,我们有一个快速的方法和一个慢的方法进行乘法:

// The `fast` and `slow` functions do the same thing
// but `slow` takes more time to complete
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)

	// we start both fast and slow in different
	// goroutines with different channels
	go fast(2, out1)
	go slow(3, out2)

	// perform some action depending on which channel
	// receives information first
	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语句由caseout1中指定的操作触发并忽略out2:

select 语句的一个常见用例是检测何时需要取消操作- 如果我们正在执行对时间敏感的操作,理想情况下我们希望保持最后期限并在花费太长时间时停止操作。

缓冲通道(Buffered Channels)

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

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

缓冲通道是一种在其中具有存储容量的通道。为了创建缓冲通道,我们在make语句中添加第二个参数来指定容量:

out := make(chan int, 3)

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

package main

import "fmt"

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

	// this statement will block
	out <- 4
}

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

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

注意事项和陷阱

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

但是,在您不需要的地方使用频道很容易。有时,使用指针和等待组来传递信息更容易。

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

如有疑问,最好在编写代码之前可视化 goroutine 之间的数据流动方式(就像我在这里的一些图表中展示的那样)。

相关推荐

Go语言泛型-泛型约束与实践(go1.7泛型)

来源:械说在Go语言中,Go泛型-泛型约束与实践部分主要探讨如何定义和使用泛型约束(Constraints),以及如何在实际开发中利用泛型进行更灵活的编程。以下是详细内容:一、什么是泛型约束?**泛型...

golang总结(golang实战教程)

基础部分Go语言有哪些优势?1简单易学:语法简洁,减少了代码的冗余。高效并发:内置强大的goroutine和channel,使并发编程更加高效且易于管理。内存管理:拥有自动垃圾回收机制,减少内...

Go 官宣:新版 Protobuf API(go pro版本)

原文作者:JoeTsai,DamienNeil和HerbieOng原文链接:https://blog.golang.org/a-new-go-api-for-protocol-buffer...

Golang开发的一些注意事项(一)(golang入门项目)

1.channel关闭后读的问题当channel关闭之后再去读取它,虽然不会引发panic,但会直接得到零值,而且ok的值为false。packagemainimport"...

golang 托盘菜单应用及打开系统默认浏览器

之前看到一个应用,用go语言编写,说是某某程序的windows图形化客户端,体验一下发现只是一个托盘,然后托盘菜单的控制面板功能直接打开本地浏览器访问程序启动的webserver网页完成gui相关功...

golang标准库每日一库之 io/ioutil

一、核心函数概览函数作用描述替代方案(Go1.16+)ioutil.ReadFile(filename)一次性读取整个文件内容(返回[]byte)os.ReadFileioutil.WriteFi...

文件类型更改器——GoLang 中的 CLI 工具

我是如何为一项琐碎的工作任务创建一个简单的工具的,你也可以上周我开始玩GoLang,它是一种由Google制作的类C编译语言,非常轻量和快速,事实上它经常在Techempower的基准测...

Go (Golang) 中的 Channels 简介(golang channel长度和容量)

这篇文章重点介绍Channels(通道)在Go中的工作方式,以及如何在代码中使用它们。在Go中,Channels是一种编程结构,它允许我们在代码的不同部分之间移动数据,通常来自不同的goro...

Golang引入泛型:Go将Interface「」替换为“Any”

现在Go将拥有泛型:Go将Interface{}替换为“Any”,这是一个类型别名:typeany=interface{}这会引入了泛型作好准备,实际上,带有泛型的Go1.18Beta...

一文带你看懂Golang最新特性(golang2.0特性)

作者:腾讯PCG代码委员会经过十余年的迭代,Go语言逐渐成为云计算时代主流的编程语言。下到云计算基础设施,上到微服务,越来越多的流行产品使用Go语言编写。可见其影响力已经非常强大。一、Go语言发展历史...

Go 每日一库之 java 转 go 遇到 Apollo?让 agollo 来平滑迁移

以下文章来源于GoOfficialBlog,作者GoOfficialBlogIntroductionagollo是Apollo的Golang客户端Apollo(阿波罗)是携程框架部门研...

Golang使用grpc详解(golang gcc)

gRPC是Google开源的一种高性能、跨语言的远程过程调用(RPC)框架,它使用ProtocolBuffers作为序列化工具,支持多种编程语言,如C++,Java,Python,Go等。gR...

Etcd服务注册与发现封装实现--golang

服务注册register.gopackageregisterimport("fmt""time"etcd3"github.com/cor...

Golang:将日志以Json格式输出到Kafka

在上一篇文章中我实现了一个支持Debug、Info、Error等多个级别的日志库,并将日志写到了磁盘文件中,代码比较简单,适合练手。有兴趣的可以通过这个链接前往:https://github.com/...

如何从 PHP 过渡到 Golang?(php转golang)

我是PHP开发者,转Go两个月了吧,记录一下使用Golang怎么一步步开发新项目。本着有坑填坑,有错改错的宗旨,从零开始,开始学习。因为我司没有专门的Golang大牛,所以我也只能一步步自己去...