Go channel功能详解
在golang中,channel属于较为核心的一个功能,尤其在go协程中,channel功能尤为重要。作为goroutine之间通信的一种方式,channel跟Linux系统中的管道/消息队列有很多类似之处。使用channel可以方便地在goroutine之间传递数据,此外,channel还关联了数据类型,如int、string等等,可以决定确定channel中的数据单元。
[TOC]
定义channel
Channel类型的定义格式如下,包括三种类型的定义。可选的<-
代表channel的方向,如果没有指定方向,那么Channel就是双向的,既可以接收数据,也可以发送数据。
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
chan T // 可以接收和发送类型为 T 的数据
chan<- float64 // 只可以用来发送 float64 类型的数据
<-chan int // 只可以用来接收 int 类型的数据
// <-总是优先和最左边的类型结合。
chan<- chan int // 等价 chan<- (chan int)
chan<- <-chan int // 等价 chan<- (<-chan int)
<-chan <-chan int // 等价 <-chan (<-chan int)
chan (<-chan int)
和slice、map类似,可以使用make关键字来初始化channel。
unbuffered := make(chan int) //定义无缓冲的整型通道
buffered := make(chan string, 10) //有缓冲的字符串通道
上述代码中定义了一个无缓冲的channel和一个有缓冲channel。make
的第一个参数需要是关键字chan
,之后跟着允许通道交换的数据的类型。如果创建的是一个有缓冲channel,之后还需要在第二个参数指定这个channel的缓冲区的大小。和其他引用类型一样,channel 的空值为 nil
,使用 ==
可以对类型相同的 channel 进行比较,只有指向相同对象或同为 nil
时,才返回 true
。
无缓冲channel默认会阻塞读取操作,有缓冲channel能部分避免阻塞读取操作。据此特性可以实现很多应用场景,后文将会逐项介绍。
读写channel
buffered <- "Gopher" //向channel buffered发送一个字符串
value := <-buffered //从channel buffered中接受一个字符串
Go使用操作符->
实现channel的读写功能。需要注意的是,在执行读写操作之前必须先初始化此通道,否则会出现永久阻塞的现象。
关闭channel
使用go内置的close
函数可以关闭channel,实际使用中经常使用 defer
功能,在程序最后关闭channel。
close(buffered)
channel用处
gorouting通信
这一点勿需多言,前面介绍channel时就提到这一点,channel的下述特性均基于此项功能实现。
gorouting同步
对于unbuffered channel,缺省情况下发送和接收会一直阻塞着,直至另一方做好准备。用此特性可以实现gororutine之间的同步功能,而不必使用显式的锁或条件变量。
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
上述代码执行结果如下
kefin@localhost:~/gopath/src/iotest $ go run sync.go
-5 17 12
上述代码是官方提供的例子,x, y := <-c, <-c
这行代码会一直处于阻塞状态,直至计算结果发送到channel中。
用于range遍历
package main
import (
"fmt"
"time"
)
func main() {
go func() {
time.Sleep(1 * time.Hour)
}()
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c)
}()
for i := range c {
fmt.Println(i)
}
fmt.Println("Finished")
}
range c
产生的迭代值为Channel中发送的值,它会一直迭代知道channel被关闭。上面的例子中如果把close(c)
注释掉,程序会一直阻塞在for …… range那一行。代码的执行结果为
kefin@localhost:~/gopath/src/iotest $ go run range.go
0
1
2
3
4
5
6
7
8
9
Finished
配合select使用
select语句选择一组可能的send操作和receive操作去处理,它类似switch,但是只是用来处理通讯(communication)操作。 它的case可以是send语句,也可以是receive语句,亦或者default。receive语句可以将值赋值给一个或者两个变量,最多允许有一个default case,它可以放在case列表的任何位置,大部分会将它放在最后。
package main
import "fmt"
func fibonacci(c, quit chan int)
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
如果有同时多个case去处理,比如同时有多个channel可以接收数据,那么Go会伪随机的选择一个case
处理(pseudo-random)。如果没有case需要处理,则会选择default
去处理。如果没有default case
,则select语句会阻塞,直到某个case需要处理。需要注意的是,nil
channel上的操作会一直被阻塞。如果没有default case
,只有nil
,那么channel的select会一直被阻塞。
此外,还可以配合select的超时处理功能,如上所述,没有case需要处理时,select语句就会一直阻塞,此时通常需要设置超时操作来处理超时的情况。 下面这个例子我们会在2秒后往channel c1中发送一个数据,但是select设置为1秒超时,因此我们会打印出timeout 1,而不是result 1。
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}
其实利用的是time.After方法,它返回一个类型为<-chan Time的单向的channel,在指定的时间发送一个当前时间给返回的channel中。执行结果为
kefin@localhost:~/gopath/src/iotest $ go run select_timeout.go
timeout 1
实现Timer和Ticker
timer是一个定时器,代表未来的一个单一事件,可以设置timer需要等待多长时间,它提供一个Channel,在将来的那个时间那个Channel提供了一个时间值。下面的例子中第二行会阻塞2秒钟左右的时间,直到时间到了才会继续执行。当然如果只想单纯的等待2秒,可以使用time.Sleep(2)
来实现。
timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 expired")
你还可以使用timer.Stop
来停止[计时器]
timer2 := time.NewTimer(time.Second)
go func() {
<-timer2.C
fmt.Println("Timer 2 expired")
}()
stop2 := timer2.Stop()
if stop2 {
fmt.Println("Timer 2 stopped")
}
ticker
是一个定时触发的计时器,它会以一个间隔(interval)往Channel发送一个事件(当前时间),而Channel的接收者可以以固定的时间间隔从Channel中读取事件。下面的例子中ticker每500毫秒触发一次,你可以观察输出的时间
ticker := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range ticker.C {
fmt.Println("Tick at", t)
}
}()
同样,ticker也可以通过Stop方法来停止。一旦它停止,接收者不再会从channel中接收数据了。
channel操作注意事项
- 关闭一个未初始化(nil) 的 channel 或者重复关闭同一个channel均会产生 panic
- 向一个已关闭的 channel 中发送消息会产生 panic
- 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已读出,则会读到类型的零值。
- 从已关闭的 channel 中读取消息永远不会阻塞,并且会返回 false ,据此可判断 channel 是否关闭
- 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息