GO基础-Channel
Channel
Channel概述
在Go语言中,channel是一种特殊的类型,用于在并发编程中实现不同的goroutine之间的通信和同步。Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。确保在数据发送和接收之间的正确顺序和时机。通过使用channel,我们可以避免在多个goroutine之间共享数据时出现的竞争条件和其他并发问题。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
// var 变量 chan 元素类型
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔值的通道
var ch3 chan []int // 声明一个传递int切片的通道
var ch4 chan struct{} // 声明一个struct的通道
它包括三种类型的定义。可选的<-
代表channel的方向。如果没有指定方向,那么Channel就是双向的,既可以接收数据,也可以发送数据。
chan T // 可以接收和发送类型为 T 的数据
chan<- float64 // 只可以用来发送 float64 类型的数据
<-chan int // 只可以用来接收 int 类型的数据
Channel创建
通道是引用类型,通道类型的空值是 nil
var c chan int
fmt.Println(c) // 结果是: <nil>
声明后的通道需要使用 make 函数初始化之后才能使用。
var c chan int // 声明一个传递整形的通道
// 初始化通道
c = make(chan int, 1) // make(chan 元素类型, [容量])
根据容量的大小可以将Channel分为无缓冲通道(阻塞),有缓存通道(非阻塞)
Channel操作
通道有发送(send)、接收(receive)和关闭(close)三种操作。发送和接收都是用符号: <-。
package main
import "fmt"
func main() {
var c chan int // 声明一个传递整形的通道
// 初始化通道
c = make(chan int, 1) // 初始化一个 有一个缓冲位的通道
c <- 1
//c <- 2 // 会报错 deadlock死锁
fmt.Println(<-c) // 取值
//fmt.Println(<-c) // 再取也会报错 deadlock死锁
c <- 2
n, ok := <-c
fmt.Println(n, ok)
close(c) // 关闭协程
c <- 3 // 关闭之后就不能再写或读了 send on closed channel
fmt.Println(c)
}
关于发送需要注意:
- send被执行前通讯一直被阻塞着。无缓存的channel只有在receiver准备好后send才被执行。如果有缓存,并且缓存未满,则send会被执行。
- 往一个已经被close的channel中继续发送数据会导致 run-time panic。
- 往nil channel中发送数据会一致被阻塞着。
关于接收需要注意:
- 从一个nil channel中接收数据会一直被block。
- 从一个被close的channel中接收数据不会被阻塞,而是立即返回,接收完已发送的数据后会返回元素类型的零值(zero value)。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
Channel循环取值
前面提到了,向关闭的Channel发送值会panic,但从一个关闭的Channel中取值是可以的
for...range
package main
import (
"fmt"
)
func main(){
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c) // 注释掉这一行的话会阻塞在这里 -- deadlock
}()
for i := range c {
fmt.Println(i)
}
fmt.Println("Finished")
}
死循环
package main
import (
"fmt"
)
func main(){
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c)
}()
for {
i, ok := <- c // 通道关闭后再取值ok=false
if !ok {
break;
}
fmt.Println(i)
}
fmt.Println("Finished")
}
select
select
语句选择一组可能的send操作和receive操作去处理。它类似switch
,但是只是用来处理通讯操作。 它的case
可以是send语句,也可以是receive语句,亦或者default
。receive
语句可以将值赋值给一个或者两个变量。它必须是一个receive操作。一个select语句最多允许有一个default case
,它可以放在case列表的任何位置,尽管我们大部分会将它放在最后。
package main
import (
"fmt"
)
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x: // 这里向c中放入值x
x, y = y, x+y
case <-quit: // 这里监听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)
}
需要注意的是,nil channel上的操作会一直被阻塞,如果没有default case,只有nil channel的select会一直被阻塞。
select
有很重要的一个应用就是超时处理。 因为上面我们提到,如果没有case需要处理,select语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况。
package main
import (
"fmt"
)
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")
}
}
这个例子会在2秒后往 c1
中发送一个数据,但是select
设置为1秒超时,因此我们会打印出timeout 1
,而不是result 1
,其实它利用的是time.After
方法,它返回一个类型为<-chan Time
的单向的channel,在指定的时间发送一个当前时间给返回的channel中。
总结:
- select中各个case执行顺序是随机的;
- 如果某个case中的channel已经ready,则执行相应的语句并退出select流程;
- 如果所有的case的channel都没有ready,则有default会走default然后退出select,没有default,select将阻塞直至channel ready;
- case后面不一定是读channel,也可以写channel,只要是对channel的操作就可以;
- 空的select语句将被阻塞,直至panic;