Go 语言入门与进阶:channel 实践(下)

1,001 阅读4分钟

这是我参与更文挑战的第 29 天,活动详情查看: 更文挑战

前文回顾

如果你还没有 Go 语言基础,建议阅读我的 从零学 Go

本系列文章,我将会进一步加深对 Go 语言的讲解,更一步介绍 Go 中的包管理、反射和并发等高级特性。

本文将会继续上一篇文章,实践 channel 的应用。。

通道 channel

Go 中倡导使用 channel 作为 goroutine 之间同步和通信的手段。channel 类型属于引用类型,且每个 channel 只能传递固定类型的数据。

如果我们创建了一个带缓冲区的 channel,那么在缓冲区未满时往 channel 发送数据将不会阻塞发送的 goroutine,代码例子如下所示:

package main

import (
	"fmt"
	"time"
)

func consume(ch chan int)  {
	// 线程休息 100s 再从 channel 读取数据
	time.Sleep(time.Second * 100)
	<- ch

}

func main()  {
	// 创建一个长度为 2 的 channel
	ch := make(chan int, 2)
	go consume(ch)

	ch <- 0
	ch <- 1
	// 发送数据不被阻塞
	fmt.Println("I am free!")
	ch <- 2
	fmt.Println("I can not go there within 100s!")

	time.Sleep(time.Second)

}

在 100 之内,输出的结果都将是:

I am free!

在缓冲区满了之后,继续往 channel 中发送数据的 goroutine 将会阻塞直到 channel 中的数据被读取,即缓存区未满时。

除了声明双向 channel,我们还可以声明单向的 channel,即只能从 channel 发送数据或者只能从 channel 中读取数据,例子如下所示:

ch := make(chan T)
// 声明只能发送的通道
var chInput chan <- int = ch
// 声明只能读取的通道
var chOutput int <- chan = ch

单向 channel 一般用于在传递 channel 值时保证代码接口的严谨性,使得只需要读取或者发送数据的仅能进行相应的操作,避免越界的误操作导致逻辑混乱。

如果在 goroutine 中需要接受多个 channel 中的消息时,我们可以使用 Go 中的 select 关键字提供的多路复用功能。select 关键字的使用方式与 switch 很类似,但是要求 case 语句后面必须为 I/O 操作,在没有 I/O 响应时且没有提供 default 语句时,goroutine 将会被阻塞。一个简单的例子如下:

package main

import (
	"fmt"
	"time"
)

func send(ch chan int, begin int )  {
	// 循环向通道发送数据
	for i :=begin ; i< begin + 10 ;i++{
		ch <- i
	}

}
func main()  {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go send(ch1, 0)
	go send(ch2, 10)

	// 主 goroutine 休眠 1s,保证调度成功
	time.Sleep(time.Second)

	for {
		select {
		case val := <- ch1: // 从 ch1 读取数据
			fmt.Printf("get value %d from ch1\n", val)
		case val := <- ch2 : // 从 ch2 读取数据
			fmt.Printf("get value %d from ch2\n", val)
		case <-time.After(2 * time.Second): // 超时设置
			fmt.Println("Time out")
			return
		}
	}

}

由于 goroutine 调度的不确定性,一个可能的运行结果为:

get value 0 from ch1
get value 10 from ch2
get value 1 from ch1
get value 11 from ch2
get value 2 from ch1
get value 12 from ch2
get value 3 from ch1
get value 13 from ch2
get value 4 from ch1
get value 5 from ch1
get value 14 from ch2
get value 6 from ch1
get value 7 from ch1
get value 8 from ch1
get value 15 from ch2
get value 9 from ch1
get value 16 from ch2
get value 17 from ch2
get value 18 from ch2
get value 19 from ch2
Time out

上述代码中,我们通过 select 多路复用分别从 ch1 和 ch2 中读取数据,如果多个 case 语句中的 ch 同时到达,那么 select 将会运行一个伪随机算法随机选择一个 case。在最后的 case 中我们设定了 <-time.After(2 * time.Second) 的 I/O 用于设定接受的最大时长为 2 s,如果时间超过 2 s,将从 select 中退出。channel 的 阻塞是无法被中断的,这是一种有效地超时从阻塞的 channel 中返回的小技巧。

小结

本文主要介绍了通道 channel 的应用实践。最为一门新生代语言,Go 的一切都显得新鲜而有趣,且在不断进步。go module 的出现有效地解决 Go 依赖混乱的问题;反射的能力也提供开发者在运行对代码进行修改的可能性,方便各种框架如依赖注入框架的实现;独特的 MPG 线程模型和 CSP 并发理念也为 Go 高性能、高并发的特性增分不少。

阅读最新文章,关注公众号:aoho求索