通道
上一次我们讲到了协程,也就是goroutine
,并且我们知道了除了goroutine
的方式之外,还有一种被称为消息通道机制的通信方式,也就是今天我们要介绍的channel
。
channel
是进程内的通信方式,因此我们可以传递包括指针在内的各种参数,但如果涉及到跨进程通信时,最好我们要考虑一下使用分布式系统的方法来进行解决,比如使用比较常见的Socket
和HTTP
等通信协议。
我们在了解channel
之前,还需要记住一句话:
我们先来看一个例子:
package main
import "fmt"
var count int = 0
func Count(ch chan int) {
ch <- count
count++
fmt.Println("Counting...", count)
}
func main() {
chs := make([]chan int, 10)
for i := 0; i < 10; i++ {
chs[i] = make(chan int)
go Count(chs[i])
}
for _, ch := range chs {
<- ch
}
}
/** The result is:
Counting...0
Counting...1
Counting...2
Counting...3
Counting...4
Counting...5
Counting...6
Counting...7
Counting...8
Counting...9
Counting...10
*/
我们在这个例子当中定义了一个包含10个channel
的数组,并且把数组中的每个channel
分配个10个不同的goroutine
,通过全局变量count
实现依次输出1-10的操作,在所有goroutine
启动完成后,通过<-ch
语句从10个channel
中依次读取数据,得到如上结果。
其实,在对应的channel
写入数据之前,这个读取数据的操作是阻塞的,这样一来,就可以使用channel
实现类似锁的功能了。也进而保证了所有goroutine
在完成后主函数才能返回。
基本语法
接下来我们先了解一些channel
的基本语法:
一般channel
的声明形式为:
var chanName chan ElemType
比如我们声明一个map
,元素是bool
型的channel
:
var m map[string] chan bool
定义一个channel
也是很简单的:
ch := make(chan int)
在声明之后,就设计到channel
的基本操作了,当然最主要的操作就是写入和读出了。
将数据写入至channel
的语法为:
ch <- value
在这里要注意,向channel
写入数据通常会导致程序的阻塞,知道有其他gotoutine
从这个channel
中读取数据。读取语法为:
value := <-ch
select 关键字
Go语言在语言级别是直接支持select
的关键字的,用于处理异步I/O问题。
其实select
和switch
是非常类似的,但select
具有更多的限制,其中最大的一个限制就是每个case
语句中必须是一个I/O操作,大致的结构如下:
select {
case <- chan1: //如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1: //如果成功向chan2写入数据,则进行该case处理语句
default: //如果上面都没有成功,就执行default中的内容
}
从上述的内容可以看出,select
并不像switch
,后面是不带判断条件的,而是直接去查看case
语句。
缓冲机制
前面的channel
都是不带缓冲的,但是在面对大量的数据的时候,单个传递的channel
(不带缓冲)就有些不太合适了,接下来介绍如何给channel
带上缓冲,从而达到消息队列的效果。
创建带缓冲的channel
:
c := make(chan int, 1024)
即在调用make()
时将缓冲区大小作为第二个参数传入即可。
这样一来,写入方可以一直往channel
里面写,在缓冲区被填完之前都不会阻塞。
超时和计时器
在前面的介绍与讲述中,并没有提及有关错误处理的任何问题,而这个问题显然是不能被忽略的,和许多地方一样,最需要考虑的一个问题就是超时的问题。
Go语言并没有提供直接处理超时的机制,但我们可以使用select
机制,通过使用select
,可以很方便的解决超时问题。
timeout := make(chan bool, 1)
go func() {
time.Sleep(1e9)
timeout <- true
}()
select {
case <- ch: // 从ch中读取到数据
case <- timeout: // 一直没有从ch中读取到数据,但从timeout中读取到了数据
}
channel的传递
Go语言中channel
本身也是一个原生类型,与map
之类的类型地位一样,因此channel
本身在定义后也可以通过channel
来传递。我们可以使用这一特性来实现Linux/UNIX中非常常见的管道pipe
特性。
/** 首先先限定基本的数据结构 */
type PipeData struct {
value int
handler func(int) int
next chan int
}
/** 一个十分简单的函数 */
func handle(queue chan *PipeData) {
for data := range queue {
data.next <- data.handler(data.value)
}
}
单向channel
顾名思义,单向channel
是只能用于发送或者接受数据的。而且定义方法也非常简单:
var ch1 chan int // ch1是一个非常正常的channel
var ch2 chan<- float64 // ch2是单向的channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读取int数据
/** 类型的强制转换对于channel来说也是可以的*/
ch4 := make(chan int)
ch5 := <-chan int(ch4)
ch6 := chan<- int(ch4)
我们设计单向
channel
的主要原因就是要保证所有代码都遵循“最小权限原则”
func Parse(ch <-chan int) {
for value := range ch {
fmt.Println("The value after parse is", value)
}
}
在这里例子当中,如果不做其他任何考虑的话,整个程序是只需要进行读操作的,因此我们将channel
定义为了单向channel
以最大化节约资源。
关闭channel
close(ch)
只需要一个close
函数即可实现channel
的关闭。
当然,如果我们希望判断一个channel
是否已经被关闭,那么我们可以使用多重返回值的方式:
x,ok := <-ch
这个用法与map
中的案件获取value
的过程比较类似,只需要看第二个bool
返回值即可,如果返回值是false
则表示ch
已经被关闭。
到这里关于Go语言中的并发内容的介绍就基本上结束了,接下来将有什么更加有趣的东西等待着我们呢?我们一同期待~