func main() {
ch := make(chan int)
ch <- 1
n := <-ch
fmt.Println(n)
}
当我们运行上面的程序时,会输出如下错误信息:
fatal error: all goroutines are asleep - deadlock!
信道可以在协程之间进行消息传递,当ch <- 1
往信道ch
中写入一个数据1
时,它会让所在的协程进入阻塞状态,等待其他协程从信道ch
中读取数据,因此n := <-ch
以及后续代码将无法得到执行的机会。也就是说,在信道未关闭的情况下,从信道读取超时会引发deadlock
异常。
deadlock
异常。正如前面提到的,信道至少涉及2个携程,因此,我们对示例一稍作修改,让它再开启一个协程。
func main() {
ch := make(chan int)
go func() {
ch <- 1
}()
n := <-ch
// fmt.Println(<-ch)
fmt.Println(n)
}
当主协程运行到n := <-ch
语句时,<-ch
会让主协程进入阻塞状态,等待其他协程往信道ch
中写入数据后再进行读取。这就保证了第03~04
行开启的子协程一定执行完成。
// fmt.Println(<-ch)
代码的注释取消掉,那么也会导致主协程再次进入阻塞后超时,由于程序没有任何一个协程再往信道ch
中写入值,无法再次读取到值,因此会抛出deadlock
错误。信道可以用close
来关闭,如果在往信道写入值后手动调用close
来关闭信道,这样其他协程即使在信道中没有写入值,协程也可以继续从信道中读取到值(零值):
func main() {
ch := make(chan int)
go func() {
ch <- 1
close(ch)
}()
n := <-ch
fmt.Println(n)
fmt.Println(<-ch)
}
上面代码中,注意第05
行close(ch)
关闭了信道ch
。关闭后,主协程可继续从ch
中读取到值,运行到fmt.Println(<-ch)
时并未抛出deadlock
错误,而是打印出值0
。这就说明协程可以继续从关闭的信道中读取到值,即使信道中没有写入数据。换句话说,当一个协程从一个没有数据的信道中读取数据且这个信道未关闭时,就会进入阻塞状态,超时后抛出deadlock
错误。
fmt.Println(<-ch)
从信道读取到的0
是写入信道的值还是int
类型对应的零值,因为二者都是0
。为了区分从已经关闭的信道中读取到的值是真正的业务数据还是对应的零值,需要用到n, ok := <-ch
这种比较优雅的读取方式,如果信道中有值,那么ok
返回true
,否则返回false
:
func main() {
ch := make(chan int)
go func() {
ch <- 0
ch <- 1
close(ch)
}()
for {
if n, ok := <-ch; ok {
fmt.Println(n)
} else {
break
}
}
fmt.Println("end")
}
在上述实例中,第04
行和第05
行分别往信道ch
中写入数据0
和1
。然后用for
循环从信道ch
中读取值并打印,如果信道已经关闭,则用break
跳出for
循环。运行程序可以看到依次输出值0
和7
。在用close
关闭信道ch
后,n, ok := <-ch
可以通过ok
来排除读取到的非业务值(零值)。
for...range
遍历信道除了用for
无限循环来遍历信道中的值外,更优雅的方式是通过for...range
来遍历信道:
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
for n := range ch {
fmt.Println(n)
}
}
在上述示例中,用for
循环依次往信道ch
中写入0~9
。第7
行close(ch)
关闭信道ch
。然后利用for n := range ch
语句可以方便快捷地对信道ch
中的数据进行遍历和读取,且不会读取关闭之后的零值。
for…range
循环遍历信道时,如果信道未关闭就会引发deadlock
错误。换句话说,用for…range
循环遍历信道,必须关闭信道。信道定义时有只读信道、只写信道以及双向信道之分。一般来说,将一个信道定义为只读或只写是没有意义的。只读信道或者只写信道一般只用于函数参数中,用于对参数进行约束,从而提高了稳定性。
一般来说,通过信道在不同的协程间进行数据传递,会涉及一个数据发送方和数据接收方,其中数据发送方本质上是一个函数,那么它只需要往信道中写入数据即可,而不用读取数据;其中的数据接收方本质上也是一个函数,那么它只需要从信道中读取数据即可,而不用写入数据。
func write(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}
func read(ch <-chan int) {
for {
n := <-ch
fmt.Println(n)
}
}
func main() {
ch := make(chan int)
go write(ch)
go read(ch)
var input string
fmt.Scanf("please input %s", input)
}
在上述示例中,函数write
的参数是ch chan<- int
类型,表示一个只写的chan int
类型信道,在函数体中,只可以对信道ch
写入int
类型的数据,而不能写入其他类型的数据或者读取数据。。
函数read
的参数是<-chan int
类型,表示一个只读的chan int
类型信道,在函数体中只可以从信道ch
中读取int
类型的数据,而不能写入类型。
write
函数体中没有调用close(ch)
关闭信道。如果关闭了信道,由于接收信道值的是for
无限循环,就会读取到零值0
,因此上述程序会一直循环,无法退出。
一般来说,如果有明确数量的数据写入信道(有限循环),那么可以在写完后用close
关闭信道,注意要用for...range
循环读取信道的数据。
如果没有明确数量的数据写入信道(无限循环),也就无法用close
关闭信道了。没有close
关闭的信道不能用for...range
循环来读取,只能用for{}
无限循环来读取信道的数据。也就是说,发送方写多少,接收方就读取多少。
在for{}
无限循环读取信道的数据时,最好有退出的条件,这样可以防止超时或者在异常的时候进行优雅的处理。这就需要用到select
关键字。
select
实现多路监听多个信道select
语块是为信道特殊设计的,它和switch
语法非常相近,都可以有多个case
块和一个default
块,但是它们之间也有很多不同。在select
语言块中,所有的case
语句要么执行信道的写入操作,要么执行信道的读取操作。
select
关键字和符号{
之间不得有任何表达式。select
语句块里面的case
语句是随机执行的,而不能是顺序执行的。
fallthrough
关键字不能用在select语句块里面。select
语句块本身不带有循环监听机制,需要通过外层for
来启用循环监听模式。在监听case
分支中,如果没有满足监听条件则进入阻塞模式。如果有多个满足条件分支则任选一个分支执行。另外,可以用default
分支来处理所有case
分支都不满足条件的情况。
func write1(ch chan<- int) {
time.Sleep(time.Second)
ch <- 1
}
func write2(ch chan<- int) {
time.Sleep(2 * time.Second)
ch <- 2
}
func main() {
ch1, ch2 := make(chan int), make(chan int)
go write1(ch1)
go write1(ch2)
for {
select {
case n := <-ch1:
fmt.Println(n)
case n := <-ch2:
fmt.Println(n)
case <-time.After(time.Second):
fmt.Println("timeout")
goto EXIT
}
}
EXIT:
var input string
fmt.Scanf("please input %s", input)
}
在上面示例中,定义了2个信道ch1
和ch2
,分别用go write1(ch1)
和go write1(ch1)
启动了2个协程。在write1
和write2
两个函数体中,先调用time.Sleep
函数休眠一段时后再往信道中写入数据。
然后用for
配合select
开启循环监听case
分支,case <-time.After(time.Second)
语句中的time.After
函数返回一个信道,其类型为<-chan Time
。这实际上是一个超时处理。如果超时,就需要跳出for
循环,此处用goto EXIT
来实现。如果用break
,只能跳出select
中的一个case
选项,而不能跳出for
循环。