单向管道
什么是单向管道
在Go语言中,管道有两种类型:双向管道与单向管道.双向管道指的是可以读也可以写,能在管道两边进行数据的读写操作,而单向管道指的是只能在管道的一边进行操作,我们手动创建一个只读/写的管道意义不大,一般是用于函数的参数传递或是作为返回值出现,例如我们用来关闭协程的管道的函数:
func close(c <- chan T
双向管道可以转换为单向管道,反过来则不可以。通常情况下,将双向管道传给某个协程或函数并且不希望它读取/发送数据,就可以用到单向管道来限制另一方的行为。
func main() { ch := make(chan int, 1) go write(ch) fmt.Println(<-ch) } func write(ch chan<- int) { // 只能对管道发送数据 ch <- 1 }
当然读管道同理
for range 遍历管道
在Go语言中我们可以基于for range来遍历读取管道中的数据,比如下面的这个例子:
package main import "fmt" func main() { ch := make(chan int, 10) defer close(ch) for i := 0; i < 10; i++ { ch <- i } for i := 0; i < 10; i++ { fmt.Println(<-ch) } }
输出为:
0 1 2 3 4 5 6 7 8 9
其实通常来说当我们使用 for range来遍历数据结构的时候,一般会有两个返回值:一个是索引,另一个则是该索引对应的位置的元素值,但是对于管道而言,有且仅有一个返回值,就是缓冲区的元素值,for range会遍历读取管道缓冲区中的元素,当管道缓冲区为空时,就会阻塞等待,直到有其他协程向管道中写入数据才会继续读取数据。
注意:关闭管道的操作尽量在发送数据的那一方进行,而不要在接收方关闭管道,因为大多数情况下接收方只知道接收数据,并不知道该在什么时候关闭管道。
select
什么是select
select
在Linux系统中一般用于IO多路复用,类似的,在Go语言中,select
是一种管道多路复用的控制结构,而到底什么是多路复用,简单的用一句话来概括的话就是:在某一时刻,同时监测多个元素是否可用,而被监测的可以是网络请求,文件IO等,而在Go语言中的select
则用于检测管道是否可用,如果可用,则读取管道中的数据,否则阻塞等待。
接下来我们来看一个简单的select的例子:
package main import "fmt" func main() { ch1, ch2, ch3 := make(chan bool), make(chan bool), make(chan bool) defer func() { close(ch1) close(ch2) close(ch3) }() select { case n, ok := <-ch1: fmt.Println(n, ok) case n, ok := <-ch2: fmt.Println(n, ok) case n, ok := <-ch3: fmt.Println(n, ok) default: fmt.Println("default") } }
与switch相似,select由多个case
和一个default
组成,default
分支可以忽略,而每一个case
只能操作一种管道,并且只能进行一种操作,要么读要么写,当有多个case可用的时候,select会随机选择一个case来执行,如果所有case都不可用,就会执行default分支,倘若没有default分支,将会阻塞等待,直到至少有一个case可用。所以上面的输出结果
为:
default
不过上述的的例子中,执行完对应分支以后,主协程就直接退出了,所以如果我们想一直检测管道的话,要给select语句加上一个死循环代码来保证select可以一直监测管道
:
package main import "fmt" func main() { ch1, ch2, ch3 := make(chan int), make(chan int), make(chan int) defer func() { close(ch1) close(ch2) close(ch3) }() go Send(ch1) go Send(ch2) go Send(ch3) for { select { case n, ok := <-ch1: fmt.Println(n, ok) case n, ok := <-ch2: fmt.Println(n, ok) case n, ok := <-ch3: fmt.Println(n, ok) } } } func Send(ch chan<- int) { for i := 0; i < 3; i++ { ch <- i } }
这样确实三个管道都能用上了,但是死循环+select会导致主协程永久阻塞,所以可以将其单独放到新协程中,并且加上一些其他的逻辑。
package main import ( "fmt" "time" ) func main() { chan1, chan2, chan3 := make(chan int), make(chan int), make(chan int) l := make(chan struct{}) defer func() { close(chan1) close(chan2) close(chan3) }() go Send(chan1) go Send(chan2) go Send(chan3) go func() { Loop: for { select { case n, ok := <-chan1: fmt.Println("A", n, ok) case n, ok := <-chan2: fmt.Println("B", n, ok) case n, ok := <-chan3: fmt.Println("C", n, ok) case <-time.After(1 * time.Second): //设置超时时间 break Loop // } } l <- struct{}{} }() <-l } func Send(ch chan<- int) { for i := 0; i < 3; i++ { ch <- i } }
上例中通过for循环配合select来一直监测三个管道是否可以用,并且第四个case是一个超时管道,超时过后便会退出循环,结束子协程。最终输出如下:
输出结果为:
A 0 true B 0 true B 1 true A 1 true A 2 true C 0 true B 2 true C 1 true C 2 true
超时机制
在是一个例子中我们用到了time.After
函数,这个函数的返回值是一个只读的管道,我们可以利用它来实现一个比较简单的超时机制,比如下面这个例子:
package main import "time" func main() { ch := make(chan struct{}, 1) go func() { time.Sleep(time.Second * 2) ch <- struct{}{} }() Loop: for { select { case <-ch: println("ok") case <-time.After(time.Second): println("超时") break Loop } } }
输出为:
超时
注意:在select的case中对值为nil的管道进行操作的话,并不会导致阻塞,该case则会被忽略,永远也不会被执行
文章知识点与官方知识档案