Channel是用来实现goroutine之间通信的数据类型,该类型是go自带的唯一一个处理高并发十分安全的一个类型。
类似于数组和切片的创建,我们也可以使用make()函数来创建管道。
c := make(chan int,5)
make函数在创建通道时有两个参数,第二个为可选参数。第一参数chan int
是类型,chan是管道类型关键字,int表示管道中传输的数据类型为int类型。
同样的如果通道中传输的数据是string类型的,如下:
c := make(chan string)
第二个参数表示通道最大容量,也就是通道最多能够缓冲的元素数量,该参数必须大于零,设置该参数的通道称为缓存通道
。
不设置第二个参数,默认通道最大容量为0,也就是不能进行缓存,这样的通道被称为非缓通道
。缓存通道与缓存通道的区别下文会讲述。
通道的发送数据和接受数据需要依靠左箭头:<-
,表示右端向左端传输数据。
c := make(chan int)
//发送数据,将 1 放入通道
c <- 1
//接收数据,取出通道中数据并且赋值给i
i := <- c
对于非缓冲通道,数据的发送和接收是要一起完成的,中间不允许存在其它操作。发送数据的goroutine在发送的数据未被接收之前,自己连同其它发送数据的goroutine都会进入阻塞状态。接收数据的gotoutine在接收到数据之前会连同其它接收的goroutine都会进入阻塞状态。
对于缓冲通道,相当于一个先进先出的队列,最先进入通道的数据一定是第一个被接收。发送数据时,将数据成功放入通道后,该通道不会进入阻塞状态,当队列装满后,想要进行发送数据的goroutine就会进入阻塞状态,当通道中出去一个元素,那么最先进行等待的goroutine就会把数据放入通道,不在阻塞。接收数据时,当通道为空时所有接收数据的goroutine就会进入阻塞状态。
注意:
当通道关闭后,如果使用接收表达式从通道获取值,会获取到对应的零值。所以常常会用到接收表达式的第二个bool类型返回值,用该值判断通道是否打开,true表示打开,false表示通道已经关闭。
panic: send on closed channel
package main
import "fmt"
func task(c chan int) {
c <- 1
}
func main() {
c := make(chan int)
go task(c)
close(c)
i := <-c
fmt.Println(i)
}
panic: close of closed channel
package main
import "fmt"
func task(c chan int) {
c <- 1
}
func main() {
c := make(chan int)
go task(c)
close(c)
close(c)
}
fatal error: all goroutines are asleep - deadlock!
对于nil通道进行close操作,会出现恐慌 —— panic: close of nil channel
上面在创建或者在传递通道时用的是chan int
类型,这是双向通道类型,既可以发送数据,也可以接受数据。除了双向通道,还有单向通道的存在:
chan<-int
—— 只能接收数据
int<-chan
—— 只能发送数据
单独使用单向通道并没有什么意义,但是go语言支持在函数或方法在传参时双向通道自动转型为单向通道,下面为例:
package main
import (
"fmt"
"time"
)
type notifier interface {
SendInt(c chan<- string)
}
type send struct {
message string
}
func (s send) SendInt(c chan<- string) {
c <- s.message
}
func main() {
s := send{message: "text......"}
ch := make(chan string)
s.SendInt(ch)
}
接口notifer中有一个参数为单向发送通道,限制了所有实现该接口的方法只能往通道中发送数据,而不能接收数据。
显然,合理使用单向通道,可以有效约束不同业务对通道的操作,避免越权使用和滥用,此外,也提高了代码的可读性,一看函数参数就可以判断出业务对通道的操作类型。
select选择语句是go语言专门为通道设计的语法,它的用法与switch语句相似,只不过case 后面只能跟通道表达式,也可以是含有通道表达式的赋值语句或短变量声明。
select {
case chan1 <- 1:
fmt.Println("chan1发送数据")
case i := <-chan2:
fmt.Println("chan2获取数据", i)
default:
//默认子句,可以没有
}
selet语句会选择一个未进入阻塞分支执行。
当所有case子句均处于阻塞状态,若有default子句就会执行该子句,若没有default子句,就会进入阻塞状态,直到有一个子句不在阻塞。
当有多个子句都未阻塞,那么会根据随机选择算法,执行其中一个子句。
我们常常通过range子句来从通道中取出数据,如下:
for i := range c {
fmt.Println(i)
}
这里需要注意几个点:
当通道中没有数据时,通道关闭后循环才会结束,在通道关闭之前循环会一直阻塞在for那一行。
在通道关闭后,循环仍然可以取出通道中剩余数据。当剩余数据全部取出后,循环结束。
package main
import (
"fmt"
"sync"
"time"
)
type event struct {
ch chan int
buffer int
handle func(num int)
timeOut time.Duration
}
func NewEvent(buffer int, timeOut time.Duration, handle func(num int)) *event {
return &(event{
ch: make(chan int, buffer),
handle: handle,
timeOut: timeOut,
})
}
func (e *event) Producer(num int) {
select {
case e.ch <- num:
e.handle(-1)
case <-time.After(e.timeOut):
}
}
func (e *event) Consumer() {
for v := range e.ch {
e.handle(v)
}
}
func main() {
var wg sync.WaitGroup
event := NewEvent(10, 2*time.Second, func(num int) {
if num == -1 {
fmt.Println("生产者生产了产品")
} else {
fmt.Println("消费者消费了第", num, "件商品")
}
})
wg.Add(1)
go func() {
for i := 0; i < 100; i++ {
event.Producer(i)
}
close(event.ch)
wg.Done()
}()
wg.Add(1)
go func() {
event.Consumer()
wg.Done()
}()
wg.Wait()
}