管道(channel)
用来在goroutine
中进行数据通信,它遵循队列数据结构,有先进先出的特性。管道是引用类型
。
// var 管道名称 chan 管道类型
var numCh chan int
var strCh chan string
var dictCh chan map[string]string
声明后还不能使用,需要对管道进行初始化。可以通过make
函数来进行初始化。
// 管道引用 = make(管道类型, 管道容量)
var ch chan int
ch = make(chan int)
var strCh chan string
strCh = make(chan string, 10)
也可以通过海象运算符进行快捷初始化:
ch := make(chan int)
管道只有三种操作:
发送和接收数据都通过操作符<-
来进行。
假设我们有一个管道:
// 创建一个容量为3的int类型管道
ch := make(chan int, 3)
发送数据时,<-
操作符在管道的后面
// 往管道ch中写入数据10
ch <- 10
接收数据时,<-
操作符在管道的前面
// 从管道ch中读取数据
recive := <-ch
通过close
函数来关闭管道
close(ch)
关闭管道需要注意一些事情:
只有在通知接收方goroutine
所有的数据都发送完毕的时候才需要关闭管道。管道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭管道不是必须的(程序结束后没关闭的管道会被垃圾回收机制自动清理掉)。
关闭后的管道有以下特点:
panic: send on closed channel
。panic: close of closed channel
。如果创建管道的时候没有指定容量,那么这个管道为无缓冲的管道,无缓冲的管道又称为阻塞的管道。
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("send successful!")
}
这段代码能够通过编译,但是在执行的时候会出现以下错误:
$ go run main.go
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/Users/augustrush/Documents/awesomeProject/main.go:12 +0x34
deadlock
表示我们程序中的goroutine
都被挂起导致程序死锁了。为什么会出现deadlock
错误呢?
因为我们使用ch := make(chan int)
创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。就像田径比赛中的4x100接力赛,想要完成交棒必须有一个能够接棒的运动员,否则只能等待。简单来说就是无缓冲的通道必须有至少一个接收方才能发送成功。`
上面的代码会阻塞在ch <- 10
这一行代码形成死锁.
还有另外一种解决上面死锁问题的方法,那就是使用有缓冲区的通道。我们可以在使用make
函数初始化通道时,可以为其指定通道的容量,例如:
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}
只要通道的容量大于零,那么该通道就属于有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
有两种方式来对管道进行遍历:
for循环
for range
通过for循环
可以遍历循环管道:
func main() {
ch := make(chan int, 10)
for i := 0; i < 10; i++ {
// 循环加入10条数据
ch <- i
}
for i := 0; i < 10; i++{
// 从管道内取出数据并打印输出
fmt.Println(<-ch)
}
}
通过这种方式不用在意管道是否关闭,即使管道关闭后,根据管道特性也可以取出管道类型对应的零值。
通过for range
可以遍历循环管道,但需注意,如果管道未关闭,通过for range
会造成deadlock
:
func main() {
ch := make(chan int, 10)
for i := 0; i < 10; i++ {
// 循环加入10条数据
ch <- i
}
for v := range(ch) {
fmt.Println(v)
}
}
输出结果:
$ go run main.go
0
1
2
3
4
5
6
7
8
9
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/Users/augustrush/Documents/awesomeProject/main.go:16 +0xac
开启两个协程,同时往同一管道执行发送与接收操作:
func main() {
// 定义管道
dataCh := make(chan int, 10)
wg.Add(1)
// 创建一个协程执行发送操作
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
dataCh <- i
fmt.Printf("【发送】数据%v已发送\n", i)
}
// 关闭管道,不然for range会造成死锁
close(dataCh)
}()
wg.Add(1)
// 创建一个协程执行接收操作
go func() {
defer wg.Done()
for v := range dataCh {
fmt.Printf("【接收】数据%v已接收\n", v)
}
}()
wg.Wait()
fmt.Println("退出...")
}
Golang中的管道可以进行读也可以进行写。但在有些场景下,只会对管道进行读操作。另一些场景下,只会对管道进行写操作。
这时,Golang就贴心的给出了两种特殊类型管道:
在默认情况下,声明的管道都是双向管道。即可读可写
// var ch chan int // 双向管道,可读可写
func main() {
ch := make(chan int, 10)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
声明一个只读管道,只需要在chan
关键字前面加上操作符<-
var ch <-chan int // 只读管道:只读不写
只读管道只能发送,当进行写操作时会报错
//
func main() {
ch := make(chan int, 10)
for i := 0; i < 10; i++ {
ch <- i
}
var readOnlyCh <-chan int = ch
fmt.Println(<-readOnlyCh)
fmt.Println(<-readOnlyCh)
// readOnlyCh <- 11
// invalid operation: cannot send to receive-only channel readOnlyCh (variable of type <-chan int)
}
声明一个只写管道,只需要在chan
关键字后面加上操作符<-
var ch chan <- int // 只写管道:只写不读
只写管道只能接收,当进行读操作时会报错
//
func main() {
onlyWriteCh := make(chan<- int, 10)
onlyWriteCh <- 1
onlyWriteCh <- 2
// fmt.Println(<-onlyWriteCh)
// invalid operation: cannot receive from send-only channel onlyWriteCh (variable of type chan<- int)
}
在某些场景下我们需要同时从多个通道接收数据,这个时候就可以用到golang
给我们提供的select
多路复用。
select
语句具有以下特点。
channel
的发送/接收操作。case
同时满足,select
会随机选择一个执行。case
的select
会一直阻塞,可用于阻塞main
函数,防止退出。select
的使用方式类似于switch
语句,它也有一系列case
分支和一个默认的分支。每个case
分支会对应一个通道的通信(接收或发送)过程。select
会一直等待,直到其中的某个case
的通信操作完成时,就会执行该case
分支对应的语句。具体格式如下:
select {
case <-ch1:
//...
case data := <-ch2:
//...
case ch3 <- 10:
//...
default:
//默认操作
}
例如:
func main() {
intChan := make(chan int, 10)
for i := 1; i <= 10; i++ {
intChan <- i
}
strChan := make(chan string, 10)
for i := 1; i <= 10; i++ {
strChan <- fmt.Sprintf("channel-%d", i)
}
for {
select {
case i := <-intChan:
fmt.Printf("从intChan获取数据:%d\n", i)
case s := <-strChan:
fmt.Printf("从strChan获取数据:%s\n", s)
default:
fmt.Println("读取数据完毕!")
return
}
}
}
上述代码运行结果如下:
$ go run main.go
从intChan获取数据:1
从strChan获取数据:channel-1
从intChan获取数据:2
从strChan获取数据:channel-2
从intChan获取数据:3
从strChan获取数据:channel-3
从intChan获取数据:4
从strChan获取数据:channel-4
从strChan获取数据:channel-5
从strChan获取数据:channel-6
从strChan获取数据:channel-7
从strChan获取数据:channel-8
从strChan获取数据:channel-9
从intChan获取数据:5
从strChan获取数据:channel-10
从intChan获取数据:6
从intChan获取数据:7
从intChan获取数据:8
从intChan获取数据:9
从intChan获取数据:10
读取数据完毕!
使用select
时需要注意以下几点:
select
一般搭配for
循环来进行操作。select
多路复用来获取管道里面的数据的时候,管道不能关闭,否则根据管道的特性(对一个关闭的管道取值会得到该管道类型对应的零值)会一直死循环。default
中进行退出,否则会一直在default
中死循环。break
跳出for
循环,需要使用return
来结束select
。基于Nginx+Supervisord+uWSGI+Django1.11.1+Python3.6.5构建