为啥要在go中使用通道
Go语言提倡通过通信共享内存而不是通过共享内存而实现通信;
channel
可以让一个goroutine
发送值到另一个goroutine
。
通道像一个队列,遵循先入先出的原则,保证收发数据的顺序。
通道是指定元素类型的,因此声明时也要带上指定的元素类型。
Channel类型
像map一样,通道是一个用make创建的数据结构引用。复制和参数传递到一个函数里的是一个引用,调用者和被调用者都引用同一份数据结构。
channel
的零值是nil。
var c chan int // 声明一个传递整形的通道
fmt.Println(c) // nil
类型比较
两个同类型的通道可以用==
比较, 通道也可以和nil
进行比较
var c2 chan int
fmt.Println(c2==c) // true
fmt.Println(c2==nil) // true
Channel操作
两种主要的操作:发送(send),接收(receive),统称为通信。send语句从一个goroutine传输一个值到另外一个goroutine。
<-
操作符可以代替发送和接受操作。
ch <- 1 // 发送操作
v := <- ch // 带赋值语句的接收操作
<- ch // 丢弃结果的接收操作
v,ok := <- ch // ok参数表示是否已经关闭通道,为true时未关闭, 为false时关闭
还有一种操作是close
关闭操作
close(ch)
关闭的通道:
- 对一个关闭的通道再发送数据,会直接PANIC。
- 对一个关闭但还有值的通道接收数据会一直获取值直到通道为空。
- 对一个已经关闭但没有值的通道接收数据会对应类型的零值。
- 对一个已经关闭的通道的接收数据操作返回的第二个参数为
bool
值,为false,未关闭时为true - 重复关闭一个通道会PAINC
关闭一个通道的操作不是必须的,只有在通知接收端所有数据都发送完毕之后才需要关闭通道。
无缓冲通道
无缓冲通道是一个阻塞通道,发送和接收goroutine都会同步化,也叫同步通道。
当在无缓冲通道中发送数据,但接收端还未执行接收操作的话,其他向通道发送数据的操作都会被阻塞,直到从通道接收值。
相反, 如果接收操作先执行,那在还未有发送操作之前, 接收端会被阻塞,直到有发送操作(另一个goroutine)
下面这个程序会在5秒之后结束运行,因为main程序在等待协程对通道ch
的发送操作
import (
"fmt"
"time"
)
var ch chan struct{}
func send() {
time.Sleep(5*time.Second)
ch <- struct{}{}
}
func main() {
start := time.Now()
ch = make(chan struct{})
go send() // 5秒之后会发送一个空结构体
<-ch
fmt.Println("持续了:", time.Since(start))
}
假如不开协程去运行会发生什么?
// go send() // 5秒后会发送一个空结构体
send() // 不使用协程运行
出现了死锁错误
fatal error: all goroutines are asleep - deadlock!
单向通道
在程序的发展中,总是(大致上)把大方法拆分成小方法,每个小方法都应尽可能的只完成自己任务,因此通道作为函数的形参时也出现了两种不同的单向通道:只读单向通道、只写单向通道。
在通道作为参数传递时,双向通道可以作为单向通道的实参,但反之则不可以。
只读单向通道:
<-chan struct{}
, 一个只能从里面接收值的通道,对其进行写操作的话,编译无法通过。
-
此外,由于关闭操作
close
需要确保没有值可以发送时才能关闭,因此关闭一个只读通道也是无法通过编译的。func read(readc <-chan int) { fmt.Println(<-readc) close(readc) } // # command-line-arguments // .\main.go:7:7: invalid operation: close(readc) (cannot close receive-only channel)
只写单向通道:chan<- struct{}
,一个只能写值到里面,但不能读取值的通道,同样的,若对其进行读操作,编译无法通过。
示例:
func read(readc <-chan int) {
fmt.Println(<-readc)
}
func write(writec chan<- int) {
writec <- 1
}
func main() {
c := make(chan int)
go write(c)
read(c)
}
缓冲通道
缓冲通道中有一个元素队列,队列的最大长度在创建通道的时候通过make的容量参数设置。
下面将创建一个可以放3个元素的缓冲通道。
c := make(chan struct{}, 3)
缓冲通道与无缓冲通道的区别就是,缓冲通道会带有一定长度的队列作为缓冲区,如上面的3。在发送前3个值时,通道并不会阻塞,而这时再发送第4个值就会阻塞掉,除非有消费端消费任意一个值。假如通道处于非满的状态(有0~2个值)是不会阻塞的,可以任意发送和读取值。
可以使用len()
函数获取当前通道有多少个元素。
比较少用的,可以用cap()
函数知道当前通道的容量是多少。
func main() {
c := make(chan struct{}, 3)
c <- struct{}{}
c <- struct{}{}
fmt.Println(len(c)) // 2
fmt.Println(cap(c)) // 3
}
循环从通道中取值
当for range一个通道时,这个通道被关闭的话,for循环会直接退出。
func main() {
c := make(chan int, 5)
for i := 0; i < 6; i++ {
go func(i int) {
c <- i
if i >= 5 {
close(c)
}
}(i)
}
for i := range c {
fmt.Println(i)
}
fmt.Println("for ends")
for {}
}
Select多道复用
Select
与switch
语句类似,有一系列情况和一个默认的可选的分支。
select {
case <- ch1:
//...
case x := <- ch2:
//...use x...
case ch3 <- y
// ...
default:
// ...
}
每个分支指定一次通信(在通道上的接收或者发送操作)和一个代码块。
select
一直等待,直到一次通信来告知有一些情况可以执行。然后它会进行这次通信并执行相应的语句;其他的通信将不会发生。
如下面的程序,每次循环都会先阻塞在select语句,知道tick事件发生或者abort事件发生
func abortAction(a chan<- struct{}) {
time.Sleep(5 * time.Second)
a <- struct{}{}
}
func main() {
t := time.Tick(time.Second)
abort := make(chan struct{})
go abortAction(abort)
for {
select {
case v := <-t:
fmt.Println("tick...:", v)
case <-abort:
fmt.Println("abort...:")
return
}
}
}
对于没有对应情况的select
(没有case),它将永远等待。
select{}
如果多种情况(case)同时发生,会随机选择一个去执行,以此保证每一个通道有相同的机会被选中。
有时候,我们在通道还未初始化完,但又不想因此而去阻塞它——非阻塞通信。可以用select
做到。select
有一种默认情况,可以在指定在没有其他的通信发生时进行相应的处理。
像下面的程序,每过一秒会打印一次default,直到abort事件发生。
func abortAction(a chan<- struct{}) {
time.Sleep(5 * time.Second)
a <- struct{}{}
}
func main() {
abort := make(chan struct{})
go abortAction(abort)
for {
select {
case <-abort:
fmt.Println("abort")
return
default:
fmt.Println("default")
time.Sleep(1 * time.Second)
}
}
}
一些特点(技巧)
-
一个已经关闭的通道,可以一直接收值(nil),也就是可以一直触发某个case,可以用来作为一个全局关闭的广播。
-
nil通道无论是发送操作还是接收操作都是阻塞的,可以放在case分支里做判断,使用场景是指定某些参数例如
--verbose
为false时创建Nil通道,否则创建普通通道var tick <-chan time.Time if verbose { tick = time.Tick(500*time.Millisecond) } // ....... select { case <-tick: print("something") default: print("default") }
本文正在参加技术专题18期-聊聊Go语言框架