Golang的通道复用上手

举报
liuzhen007 发表于 2021/07/25 16:50:16 2021/07/25
【摘要】 目录前言正文结尾 前言今天我们来聊一聊 Golang 中的通道,我们可以使用通道来传输数据,也可以传递消息,多个协程之间就是通过通道来通讯的。 正文在 Golang 中如何表示通道呢?通道的关键字是 chan,但它是有类型,可以是整型、字符型、布尔型等。每个通道都有属于自己的类型,该类型表示通道中允许传递的数据类型,这一点是严格规定。通道作为一种数据类型,也有自己的默认值,零值为 nil, ...

目录

  • 前言
  • 正文
  • 结尾

前言

今天我们来聊一聊 Golang 中的通道,我们可以使用通道来传输数据,也可以传递消息,多个协程之间就是通过通道来通讯的。

正文

在 Golang 中如何表示通道呢?通道的关键字是 chan,但它是有类型,可以是整型、字符型、布尔型等。每个通道都有属于自己的类型,该类型表示通道中允许传递的数据类型,这一点是严格规定。通道作为一种数据类型,也有自己的默认值,零值为 nil, 通道必须使用 make() 方法来定义创建。

接下来,我们通过一段代码来理解一下吧。

实例代码:

package main

import (
	"fmt"
)

func main() {
	//通道的声明
	var channel chan int
	if channel == nil {
		fmt.Println("我是通道 channel")
		fmt.Println("我刚被声明,还没有定义,因此是 nil")
	}
	//如果通道时nil 则要通过make创建通道
	channel= make(chan int)
	if channel != nil {
		fmt.Println("我是通道 channel")
		fmt.Println("我刚被make定义了,已经不是 nil 了")
	}
	
}

代码执行结果:

我是通道 channel

我刚被声明,还没有定义,因此是 nil

我是通道 channel

我刚被make定义了,已经不是 nil 了

通过上面的代码,我们可以知道通道变量是通过关键字 chan 来声明的,同时需要指明通道内传输的数据的类型。另外,通道声明后是空值,需要使用 make() 方法来定义创建。

下面,我们来看看通道变量的类型和值。

完善上面的代码,修改如下:

package main

import (
	"fmt"
)

func main() {
	//通道的声明
	var channel chan int
	if channel == nil {
		fmt.Println("我是通道 channel")
		fmt.Println("我刚被声明,还没有定义,因此是 nil")
	}
	//如果通道时nil 则要通过make创建通道
	channel= make(chan int)
	if channel != nil {
		fmt.Println("我是通道 channel")
		fmt.Println("我刚被make定义了,已经不是 nil 了")
		fmt.Printf("我的通道数据类型:%T,通道的值:%v,\n", channel, channel)
	}
	
}

代码执行结果:

我是通道 channel

我刚被声明,还没有定义,因此是 nil

我是通道 channel

我刚被make定义了,已经不是 nil 了

我的通道数据类型:chan int,通道的值:0xc000064060,

我们发现,通道的数据类型是 chan int,而不是 int。

另外,通道的值是一个地址,一个内存地址 0xc000064060 ,所以说通道一个引用类型的变量。

通道既然是用来传输数据的,那么这些数据一定有自己的类型,是的,没错。同样管道也有自己的类型,只是在数据类型的前面多了一个 chan 标识,比如 chan int,chan string等。

通道既然是用来传输数据的,那么通道会不会有方向的区别呢?哈哈,是的。通道确实是有方向的,只不过,我们一般都是常见双向通道。注意,这里说的方向是数据的写入和读取。

这时,可能有人会问:是不是有些通道,只能来读数据?有些通道,只能来写数据?
答案是 yes 。

那么如何声明和定义只读通道和只写通道呢?

接下来,看一段代码:

ch1 := make(chan int)   // 一般情况下,我们定义的双向通道
ch2 := make(chan<- int) // 这是一个只写通道,只能写入int类型的数据
ch3 := make(<-chan int) // 这是一个只读通道,只能读取int类型的数据

好的,明白来了吧。

那么通道还有别的类型吗?

有的,通道还有一种类型————缓冲通道。

一般创建的通道,其实也是有缓冲区的,只是缓冲区大小为0。嘿嘿,就是没有嘛!

缓冲通道怎么声明和定义呢?我们再来看一段代码:

ch1 := make(chan int)     // 一般情况下,我们定义的缓冲区为0的通道
ch2 := make(chan int, 10) // 我们定义了一个缓冲区大小为10的通道,意思就是说我们可以写入11个数据,不理解的话,可以好好思考一下呦!

那么缓冲通道有什么特殊的作用呢?一般的非缓冲通道,每次读写都是阻塞的,很容易造效率问题,而缓冲通道就可以在一定程度上避免这个问题。

下面我们通过一个例子,来看看缓冲通道是怎么工作的吧。

实例代码:

package main

import (
	"fmt"
)

func main() {
	// 定义一个缓冲区大小为5的通道
	ch1 := make(chan int, 5)

	// 开启子协程goroutine写入数据
	go func() {
		for i := 0; i < 11; i++ {
			ch1 <- i
			fmt.Println("子协程写入数据:", i)
		}
		close(ch1) //关闭通道
	}()

	// 主协程读取数据
	for {
		v, ok := <-ch1
		if !ok {
			fmt.Println("读取结束", ok)
			break
		}
		fmt.Println("主协程读取到的数据为:", v)
	}

	fmt.Println("主协程结束")
}

代码执行过程:

子协程写入数据: 0

子协程写入数据: 1

子协程写入数据: 2

子协程写入数据: 3

子协程写入数据: 4

子协程写入数据: 5

主协程读取到的数据为: 0

主协程读取到的数据为: 1

主协程读取到的数据为: 2

主协程读取到的数据为: 3

主协程读取到的数据为: 4

主协程读取到的数据为: 5

主协程读取到的数据为: 6

子协程写入数据: 6

子协程写入数据: 7

子协程写入数据: 8

子协程写入数据: 9

子协程写入数据: 10

主协程读取到的数据为: 7

主协程读取到的数据为: 8

主协程读取到的数据为: 9

主协程读取到的数据为: 10

读取结束 false

主协程结束

我们可以看到,缓冲通道能够连续写入数据,即使没有被读取,也不会被阻塞。这样就能够避免像非缓冲通道那样一写一读的死板工作方式,当然这也需要看具体的使用场景,不能一概而论。

既然我们已经打算实现多个通道的统一管理,换句话说就是多路复用,我们需要一个方向。先来看看目前的通道的状态,每个通道都有自己的处理协程。

我们看段代码:

package main

import (
	"fmt"
)

func main() {
	// 定义一个通道ch1
	ch1 := make(chan int)

	// 开启子协程goroutine写入数据
	go func() {
		for i := 0; i < 11; i++ {
			ch1 <- i
			fmt.Println("子协程ch1写入数据:", i)
		}
		close(ch1) //关闭通道
	}()
        
	// 定义一个通道ch2
	ch2 := make(chan int)

	// 开启子协程goroutine写入数据
	go func() {
		for i := 0; i < 11; i++ {
			ch2 <- i
			fmt.Println("子协程ch2写入数据:", i)
		}
		close(ch2) //关闭通道
	}()

	// 主协程读取ch1数据
	for {
		v, ok := <-ch1
		if !ok {
			fmt.Println("读取结束", ok)
			break
		}
		fmt.Println("主协程读取到ch1数据为:", v)
	}

	// 主协程读取ch2数据
	for {
		v, ok := <-ch2
		if !ok {
			fmt.Println("读取结束", ok)
			break
		}
		fmt.Println("主协程读取到ch2数据为:", v)
	}

	fmt.Println("主协程结束")
}

执行结果如下:

子协程ch1写入数据: 0
主协程读取到ch1数据为: 0
主协程读取到ch1数据为: 1
子协程ch1写入数据: 1
子协程ch1写入数据: 2
主协程读取到ch1数据为: 2
主协程读取到ch1数据为: 3
子协程ch1写入数据: 3
子协程ch1写入数据: 4
主协程读取到ch1数据为: 4
主协程读取到ch1数据为: 5
子协程ch1写入数据: 5
子协程ch1写入数据: 6
主协程读取到ch1数据为: 6
主协程读取到ch1数据为: 7
子协程ch1写入数据: 7
子协程ch1写入数据: 8
主协程读取到ch1数据为: 8
主协程读取到ch1数据为: 9
子协程ch1写入数据: 9
子协程ch1写入数据: 10
主协程读取到ch1数据为: 10
读取结束 false
主协程读取到ch2数据为: 0
子协程ch2写入数据: 0
子协程ch2写入数据: 1
主协程读取到ch2数据为: 1
主协程读取到ch2数据为: 2
子协程ch2写入数据: 2
子协程ch2写入数据: 3
主协程读取到ch2数据为: 3
主协程读取到ch2数据为: 4
子协程ch2写入数据: 4
子协程ch2写入数据: 5
主协程读取到ch2数据为: 5
主协程读取到ch2数据为: 6
子协程ch2写入数据: 6
子协程ch2写入数据: 7
主协程读取到ch2数据为: 7
主协程读取到ch2数据为: 8
子协程ch2写入数据: 8
子协程ch2写入数据: 9
主协程读取到ch2数据为: 9
主协程读取到ch2数据为: 10
子协程ch2写入数据: 10
读取结束 false
主协程结束

通过上面的代码,我们也发现每个通道都有自己的协程,然后再和主协程进行数据通讯。这样非常的繁琐,很多代码都是重复的。有没有办法实现通道复用呢?

答案是有的。

我们想要实现的效果是使用一个协程处理所有的通道的通讯,这就需要用到 select 关键字,它都能做些什么呢?

接下来,我们通过一端代码了解一下。

代码实例:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go func() {
		for {
			select {
			case c1 := <-ch1:
				fmt.Println("成功获取通道ch1的数据:", c1)
			case c2 := <-ch2:
				fmt.Println("成功获取通道ch2的数据:", c2)
			case <-time.After(time.Second * 2):
			    //使用time.After 设置超时响应。
				fmt.Println("超时!!")
			}
		}
	}()

	for i := 0; i < 10; i++ {
		ch1 <- i
		fmt.Println("通道ch1写入数据:", i)
	}
	for i := 0; i < 10; i++ {
		ch2 <- i
		fmt.Println("通道ch2写入数据:", i)
	}
	time.Sleep(20) // 等待select所在协程执行结束
}

代码执行结果:

成功获取通道ch1的数据: 0
通道ch1写入数据: 0
通道ch1写入数据: 1
成功获取通道ch1的数据: 1
成功获取通道ch1的数据: 2
通道ch1写入数据: 2
通道ch1写入数据: 3
成功获取通道ch1的数据: 3
成功获取通道ch1的数据: 4
通道ch1写入数据: 4
通道ch1写入数据: 5
成功获取通道ch1的数据: 5
成功获取通道ch1的数据: 6
通道ch1写入数据: 6
通道ch1写入数据: 7
成功获取通道ch1的数据: 7
成功获取通道ch1的数据: 8
通道ch1写入数据: 8
通道ch1写入数据: 9
成功获取通道ch1的数据: 9
成功获取通道ch2的数据: 0
通道ch2写入数据: 0
通道ch2写入数据: 1
成功获取通道ch2的数据: 1
成功获取通道ch2的数据: 2
通道ch2写入数据: 2
通道ch2写入数据: 3
成功获取通道ch2的数据: 3
成功获取通道ch2的数据: 4
通道ch2写入数据: 4
通道ch2写入数据: 5
成功获取通道ch2的数据: 5
成功获取通道ch2的数据: 6
通道ch2写入数据: 6
通道ch2写入数据: 7
成功获取通道ch2的数据: 7
成功获取通道ch2的数据: 8
通道ch2写入数据: 8
通道ch2写入数据: 9
成功获取通道ch2的数据: 9

大家看到,上面的代码,我们只通过一个匿名协程就可以处理所有通道的消息,实现了通道的复用。而且整个过程也非常的明确,管理起来也非常方便。

结尾

通道在 Golang 的实际使用场景中非常常见,所以我们需要认知学习。其实,Golang的通道的管理是非常复杂的,今后我们在 Golang 的学习过程中,需要重点关注。好了,今天关于通道的介绍就到这里,谢谢大家。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。