golang语言异步通信之Channel
简介
Channel主要用在go routine之间作为异步通信工具。
简单说我们可以把Channel理解为一个队列,有人负责往里面写,有人负责从里面读,Channel会保证读和写操作的时序性和原子性,先写入的数据一定先读出来,而且一次读写都是一个完整的数据类型。
基本用法
ch <- val // 把值val写入Channel ch
val := <-ch // 从Channel ch中读取值,并且写入变量val
操作符号是<-,箭头向指明数据的流向,注意没有相对的->操作符,
创建Channel
ch := make(chan int)
或者
var ch chan int
ch = make(chan int, 1)
和map、slice一样,chan必须先make出来然后才能使用。
注意make的第二个参数,用来表明chan的缓冲大小,表明允许写入的数据的个数,在缓冲没有满之前,写操作直接返回,而一旦缓冲满了,则写操作会被阻塞,只到有新的缓冲空间释放出来。
而如果大小为0,则表示没有缓冲,则任何写入操作都会阻塞,知道有读请求进来。
关闭 Channel
close(ch)
一个Channel一旦被关闭了,就不再允许继续往里面写入数据,否则会引发panic错误
panic: send on closed channel
注意,panic只针对写操作,而对于读操作不会panic,而是会立即返回:
- 如果Channel里面还有数据,则正常返回读取的数据,就像Channel没有关闭一样。
- 如果Channel里面已经没有数据,则返回对应数据类型的零值。如果是int则返回0,如果是string则返回""
另外可以使用一个额外的参数来检查channel是否已经被关闭了(以示区别读取的是一个真的零值,还是channel已经关闭返回的零值)。
v, ok := <- ch
如果ok 是false,表明这个channely已经被关闭了。
Range处理Channel
for i := range ch {
fmt.Println(i)
}
这个for循环会一直迭代,直到channel被关闭;如果ch里面已经没有数据了,for循环会阻塞,只到新的数据进来。
select操作
select语句语法类似switch,但只能用来处理Channel相关的操作。
它的分支(case)可以是Channel的读语句,也可以是Channel的写语句,或者是default语句。
- 对于读case语句,如果当前Channel有数据可读,则执行。
- 对于写case语句,如果当前Channel有缓冲可写,则执行。
- 如果既没有可读,又没有可写,则执行default语句,如果没有default语句,则阻塞select语句。
- 例子 1
这个例子走写的分支,即会打印"put value to chan",因为ch没有数据第一个case读操作不满足,第二个case写可以写入,因为Channel有一个缓冲大小。
func main() {
ch := make(chan int, 1)
select {
case i := <- ch:
fmt.Printf("get value from chan: %d\n", i)
case ch <- 1:
fmt.Printf("put value to chan\n")
}
}
- 例子 2
这个例子会阻塞,因为没有数据读,而又没有缓冲,写也不能成功。
func main() {
ch := make(chan int)
select {
case i := <- ch:
fmt.Printf("get value from chan: %d\n", i)
case ch <- 1:
fmt.Printf("put value to chan\n")
}
}
- 例子 3:
这个例子会走到default分支,打印出"select default branch",因为读和写的分支都不满足,又有default分支。
func main() {
ch := make(chan int)
select {
case i := <- ch:
fmt.Printf("get value from chan: %d\n", i)
case ch <- 12:
fmt.Printf("put value to chan\n")
default:
fmt.Printf("select default branch\n")
}
}
select的Timeout
前面我们看到如果select语句没有分支(case)语句能够处理,而又当没有定义default分支时,select会一直阻塞,这时候就需要一个超时机制。
func main() {
ch := make(chan int, 1)
select {
case i := <- ch:
fmt.Printf("get value from chan: %d\n", i)
case <-time.After(time.Second * 2):
fmt.Println("timeout 2 seconds")
}
}
这个例子里,两秒之后超时会触发,打印"timeout 2 seconds"。
这个超时机制听起来很高级,其实它就是利用的是time.After方法,我们看下time.After的定义:
# time/sleep.go
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
...
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer{
C: c,
r: runtimeTimer{
when: when(d),
f: sendTime,
arg: c,
},
}
startTimer(&t.r)
return t
}
我们看到time.After()返回的是一个类型为<-chan Time的单向的channel,这个channel里面有一个值,是一个时间戳值,从而保证在时间到期之后返回的channel里面有一个值,那么这个分支的读取操作能够满足不会阻塞。
想到另外一个场景,select的各个分支都是函数,函数返回一个Channel类型值。
func foo1(ch chan int) chan int {
fmt.Printf("in foo1\n")
return ch
}
func foo2(ch chan int) chan int {
fmt.Printf("in foo2\n")
return ch
}
func main() {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch1 <- 11
ch2 <- 22
select {
case i := <- foo1(ch1):
fmt.Printf("get value from chan1: %d\n", i)
case i := <- foo2(ch2):
fmt.Printf("get value from chan2: %d\n", i)
}
}
试猜想执行输出结果是什么?是下面这个吗?
in foo1
get value from chan1: 11
很多人第一直觉应该是的;我们运行一下看看结果:
$ go build && ./main
in foo1
in foo2
get value from chan1: 11
$ go build && ./main
in foo1
in foo2
get value from chan1: 11
$ go build && ./main
in foo1
in foo2
get value from chan2: 22
上面运行了三次,注意两点:
- select的两个分支任何一个都可能被执行到,他们是随机的。即当select的多个分支都满足条件时,哪一个会被选中执行是随机的,不是严格按照代码顺序。
- 不管执行了哪一个select分支,函数foo1()和函数foo2()都被执行了。这个怎么理解呢,因为select语句关注的是channel对象,只有执行了函数foo1()和foo2()才能从返回值中拿到Channel对象,从而才能知道Channel是否能够满足读或者写的需求,即只有找到各个select分支的Channel对象本身,才能判断Channel的状态。