chan
是 Golang 中内置的数据类型,不像Mutex
等需要引入,它是 first-class 类型,在 Go 的并发控制中起着相当重要的作用。chan 的思想来自 Tony Hoare 在 1978 年发表的论文 Communicating Sequential Processes, 它提出了一种并发的编程语言,用来描述并发系统中的互动模式,后来逐渐形成了 CSP 并发模式。CSP 模式中存在 Process/Channel 每个 Process 独立运行,多个 Process 之间通过 Channel 通信。Go 并发控制的核心思想:
Don’t communicate by sharing memory, share memory by communicating. 即不要通过共享内存来通信,而是要通过通信来共享内存,
前一句话对应传统利用 锁 等方式控制并发,后者就对应 CSP方式,go 中的 goroutine/chan 分别与 CSP 中的 Process/Channel 对应。
基本分类
chan // 既可以发送数据,又可以接收数据的 channel
chan<- // 只能接收数据的 channel
<-chan // 只能发送数据的 channel
channel 是管道,因此一个传输具体类型数据的 channel 声明方式如下:
var ch chan string // 双向通道, 既可以往 ch 中写,也可以从 ch 中读 string 类型数据
var ch chan<- interface{} 或者 var ch = make(chan<- interface{}) // 只能往 ch 中发送 struct{} 类型数据
var ch <-chan int 或者 var ch make(<-chan int) // 只能从 ch 中读取 int 数据
// 只读和只写一般用于数据通道,单独使用没有意义,比如:
func main() {
c := make(chan int)
go send(c)
go recv(c)
time.Sleep( * time.Second)
}
//只能向chan里写数据
func send(c chan<- int) {
for i := ; i < ; i++ {
c <- i
}
}
//只能取channel中的数据
func recv(c <-chan int) {
for i := range c {
fmt.Println(i)
}
}
channel 使用必须初始化,初始化是否指定长度可以分为
ch := make(chan string) // 无缓冲通道
ch := make(chan string, 2)// 有缓冲通道
往 channel 中发送数据使用 “ch<-”,示例如下:
ch <- 10
这里的 ch 的类型是 chan int
或者 chan<- int
。
从 channel 中接收数据使用 “<-ch”,示例如下:
x := <-ch // 把 ch 中接收到的一条数据赋值给 x
foo(<-ch) // 把 ch 中接收到的一条数据作为函数 foo 的参数
<-ch // 丢弃一条从 ch 中接收到的数据
从 channel 中接收数据的时候,还可以接收两个值:
x, ok := <-ch
ok 是一个 bool 值,表示是否成功的从 channel 中接收到了数据。如果 ok 是 false,ch 已经被 close,且 ch 中没有缓存数据,那么 x 就是零值。所以,如果 x 是零值,有可能是接收到了零值,也有可能是空的且被 close 的 channel 产生的零值。
Go 的内建函数 close、cap、len 都可以操作 channel。close 可以关闭 channel,关闭之后的 channel 无法接收数据;cap 返回 channel 的容量;len 返回 channel 的长度。
发送和接收数据还可以作为 select 语句的 case clause,例如:
func main() {
var ch = make(chan int, 10)
for i := 0; i < 10; i++ {
select {
case ch <- i:
case v := <-ch:
fmt.Println(v)
}
}
}
channel 还可以用于 for-range 语句中:
for v := range ch {
fmt.Println(v)
}
或者是忽略读取的值,只是清空 channel:
for range ch {
}
常见的会导致 panic 的三种场景如下:
无缓冲chan的使用,必须要求接收方和发送方都准备好,如果仅仅发送方准备好就发送信息,会导致chan阻塞,例如:
var ch = make(chan int)
ch <- -1
for {
select {
case res := <-ch:
fmt.Println("收到信息",res)
}
}
该示例中,对于无缓冲信道,在接收者未准备好之前,发送操作是阻塞的.,goroutine 无法退出,进而造成泄露。
解决方案1:让接收者提前做好准备,随时准备接受信息,推荐使用。
var ch = make(chan int)
go func() {
for {
select {
case res := <-ch:
fmt.Println("收到信息",res)
}
}
}()
ch <- -1
解决方案2:使用有缓冲通道
var ch = make(chan int,1)
ch <- -1
fmt.Println("收到信息", <-ch)
每个缓冲信道,都有容量,当信道里的数据量等于信道的容量后,此时再往信道里发送数据,就失造成阻塞,必须等到有人从信道中消费数据后,程序才会往下进行。
func main() {
ch1 := make(chan string, 1)
ch1 <- "hello world"
ch1 <- "hello China"// 超过容量,排队阻塞
fmt.Println(<-ch1)
}
当程序一直在等待从信道里读取数据,而此时并没有人会往信道中写入数据。此时程序就会陷入死循环,造成死锁。
func main() {
pipline := make(chan string)
go func() {
pipline <- "hello world"
pipline <- "hello China"
}()
for data := range pipline{
fmt.Println(data)
}
}
解决:只要在发送完数据后,手动关闭信道,告诉 range 信道已经关闭,无需等待就行。
func main() {
pipline := make(chan string)
go func() {
pipline <- "hello world"
pipline <- "hello China"
close(pipline)
}()
for data := range pipline{
fmt.Println(data)
}
}