Contents
8.3. 通道(channel)-在多个goroutine间通信的管道¶
单纯地将函数并发执行是没有意义的。 函数与函数间需要交换数据才能体现并发执行函数的意义。 虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。 为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言提倡使用通信的方法代替共享内存,这里通信的方法就是使用通道(channel).
提示:
在地铁站、食堂、洗手间等公共场所人很多的情况下,大家养成了排队的习惯, 目的也是避免拥挤、插队导致的低效的资源使用和交换过程。
代码与数据也是如此,多个goroutine为了争抢数据,势必造成执行的低效率,使用队列的方式是最高效的,channel 就是一种队列一样的结构。
8.3.1. 1.通道的特性¶
Go语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个goroutine访问通道进行发送和获取数据。goroutine间通过通道就可以通信。
通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
8.3.2. 2.声明通道类型¶
通道本身需要一个类型进行修饰,就像切片类型需要标识元素类型。通道的元素类型就是在其内部传输的数据类型,声明如下:
var 通道变量 chan 通道类型
·通道类型:通道内的数据类型。
·通道变量:保存通道的变量。
chan 类型的空值是 nil,声明后需要配合 make 后才能使用。
8.3.3. 3.创建通道¶
通道是引用类型,需要使用 make 进行创建,格式如下:
通道实例 := make(chan 数据类型)
· 数据类型:通道内传输的元素类型。
· 通道实例:通过make创建的通道句柄。
例如:
ch1 := make(chan int) // 创建一个整型类型的通道
ch2 := make(chan interface{}) // 创建一个空接口类型的通道,可以存放任意格式
type Equip struct{ /* 一些字段 */ }
ch2 := make(chan *Equip) // 创建Equip指针类型的通道,可以存放*Equip
// 无缓冲的整型通道
unbuffered := make(chan int)
// 有缓冲的字符串通道
buffered := make(chan string, 10)
ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3
向通道发送值
// 有缓冲的字符串通道
buffered := make(chan string, 10)
// 通过通道发送一个字符串
buffered <- "Gopher"
从通道里接收值
// 从通道接收一个字符串
value := <-buffered
当从通道里接收一个值或者指针时,<-
运算符在要操作的通道变量的左侧。
3.1 例子¶
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan interface{})
type Equip struct{
name string
age int
}
ch3 := make(chan *Equip)
fmt.Printf("%#v\n",ch1) //(chan int)(0xc000012180)
fmt.Printf("%#v\n",ch2) //(chan interface {})(0xc0000121e0)
fmt.Printf("%#v\n",ch3) //(chan *main.Equip)(0xc000012240)
}
8.3.4. 4.使用通道发送数据¶
通道创建后,就可以使用通道进行发送和接收操作。
1) 通道发送数据的格式¶
通道的发送使用特殊的操作符<-,将数据通过通道发送的格式为:
通道变量 <- 值
· 通道变量:通过make创建好的通道实例。
· 值:可以是变量、常量、表达式或者函数返回值等。值的类型必须与ch通道的元素类型一致。
2) 通过通道发送数据的例子¶
使用 make 创建一个通道后,就可以使用<-向通道发送数据,代码如下:
// 创建一个空接口通道
ch := make(chan interface{})
// 将0放入通道中
ch <- 0
// 将hello字符串放入通道中
ch <- "hello"
3) 发送将持续阻塞,直到数据被接收¶
把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。 Go程序运行时能智能地发现一些永远无法发送成功的语句并做出提示,代码如下:
package main
// 通过通道发送数据的例子
func main() {
// 创建一个整型通道
ch := make(chan int)
// 尝试将0通过通道发送
ch <- 0
}
/*
fatal error: all goroutines are asleep - deadlock!
*/
报错的意思是:运行时发现所有的goroutine(包括main)都处于等待goroutine。 也就是说所有goroutine中的channel并没有形成发送和接收对应的代码。
8.3.5. 5.使用通道接收数据¶
通道接收同样使用<-操作符,通道接收有如下特性:
① 通道的收发操作在不同的两个 goroutine 间进行。
由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个goroutine中进行。
② 接收将持续阻塞直到发送方发送数据。
如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。
③ 每次接收一个元素。
通道一次只能接收一个数据元素。
5.1 通道的数据接收4种写法。¶
1.阻塞接收数据¶
阻塞模式接收数据时,将接收变量作为“<-”操作符的左值,格式如下:
data := <-ch
执行该语句时将会阻塞,直到接收到数据并赋值给data变量。
2.非阻塞接收数据¶
使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:
data, ok := <-ch
data:表示接收到的数据。未接收到数据时,data为通道类型的零值。
ok:表示是否接收到数据。
非阻塞的通道接收方法可能造成高的CPU占用,因此使用非常少。如果需要实现接收超时检测,可以配合select和计时器channel进行。
3.接收任意数据,忽略接收的数据¶
阻塞接收数据后,忽略从通道返回的数据,格式如下:
<-ch
执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。 这个方式实际上只是通过通道在goroutine间阻塞收发实现并发同步。
使用通道做并发同步的写法,可以参考下面的例子:
package main
import (
"fmt"
)
func main() {
// 构建一个通道
ch := make(chan int)
// 开启一个并发匿名函数
go func() {
fmt.Println("start goroutine")
// 通过通道通知main的goroutine
ch <- 0
fmt.Println("exit goroutine")
}()
fmt.Println("wait goroutine")
// 等待匿名goroutine
<-ch
fmt.Println("all done")
}
执行代码,输出如下:
wait goroutine
start goroutine
exit goroutine
all done
4.循环接收¶
通道的数据接收可以借用for range语句进行多个元素的接收操作,格式如下:
for data := range ch {
}
通道ch是可以进行遍历的,遍历的结果就是接收到的数据。 数据类型就是通道的数据类型。通过for遍历获得的变量只有一个,即上面例子中的data。
遍历通道数据的例子
package main
import (
"fmt"
"time"
)
func main() {
// 构建一个通道
ch := make(chan int)
// 开启一个并发匿名函数
go func() {
// 从3循环到0
for i := 3; i >= 0; i-- {
// 发送3到0之间的数值
ch <- i
// 每次发送完时等待
time.Sleep(time.Second)
}
}()
// 遍历接收通道数据
for data := range ch {
// 打印通道数据
fmt.Println(data)
// 当遇到数据0时,退出接收循环
// 当接收到数值0时,停止接收。如果继续发送,由于接收goroutine已经退出,没有goroutine发送到通道,因此运行时将会触发宕机报错。
if data == 0 {
break
}
}
}
5.2示例-并发打印¶
package main
import "fmt"
func printer(c chan int) {
// 开始无限循环等待数据
for {
// 从channel中获取一个数据
data := <-c
// 将0视为数据结束
if data == 0 {
break
}
// 打印数据
fmt.Println(data)
}
// 通知main已经结束循环 (我搞定了!)
c <- 0
}
func main() {
// 创建一个channel
c := make(chan int)
// 并发执行printer,传入channel
go printer(c)
for i := 1; i <= 10; i++ {
//将数据通过channel投送给printer
c <- i
}
// 通知并发的printer结束循环(没有数据啦!)
c <- 0
//等待printer结束(搞定喊我!)
<-c
}
/*
1
2
3
4
5
6
7
8
9
10
*/
5.4示例-同时发送和接收(不带缓冲的通道)¶
package main
import (
"fmt"
"time"
)
/*
使用make来建立一个通道:
var channel chan int = make(chan int)
// 或
channel := make(chan int)
// 定义接收的channel
receive_only := make (<-chan int)
// 定义发送的channel
send_only := make (chan<- int)
// 可同时发送接收
send_receive := make (chan int)
· chan<- 表示数据进入通道,要把数据写进通道,对于调用者就是发送。
· <-chan 表示数据从通道出来,对于调用者就是得到通道的数据,当然就是接收。
定义只发送或只接收的channel意义不大,一般用于在参数传递中:
*/
func main() {
c := make(chan int) // 不使用带缓冲区的channel
go send(c)
go recv(c)
time.Sleep(3 * time.Second)
close(c)
}
// 只能向chan里send数据
func send(c chan<- int) {
for i := 0; i < 10; i++ {
fmt.Println("send readey", i)
c <- i
fmt.Println("send", i)
}
}
// 只能接收channel中的数据
func recv(c <-chan int) {
for i := range c{
fmt.Println("received", i)
}
}
/*
send readey 0
send 0
send readey 1
received 0
received 1
send 1
.....
....
*/
8.3.6. 6.单向通道—通道中的单行道¶
Go的通道可以在声明时约束其操作方向,如只发送或是只接收。这种被约束方向的通道被称做单向通道。
6.1 单向通道的声明格式¶
只能发送的通道类型为chan<-,只能接收的通道类型为<-chan,格式如下:
var 通道实例 chan<- 元素类型
var 通道实例 <-chan 元素类型
元素类型:通道包含的元素类型。
通道实例:声明的通道变量。
6.2 单向通道的使用例子¶
示例代码如下:
ch := make(chan int)
// 声明一个只能发送的通道类型,并赋值为ch
var chSendOnly chan<- int = ch
//声明一个只能接收的通道类型,并赋值为ch
var chRecvOnly <-chan int = ch
上面的例子中,chSendOnly只能发送数据,如果尝试接收数据,将会出现如下报错:
invalid operation: <-chSendOnly (receive from send-only type chan<-
同理,chRecvOnly也是不能发送的。
当然,使用make创建通道时,也可以创建一个只发送或只读取的通道:
ch := make(<-chan int)
var chReadOnly <-chan int = ch
<-chReadOnly
上面代码编译正常,运行也是正确的。但是,一个不能填充数据(发送)只能读取的通道是毫无意义的。
6.3 time包中的单向通道¶
time包中的计时器会返回一个timer实例,代码如下:
timer := time.NewTimer(time.Second)
timer的Timer类型定义如下:
type Timer struct {
// C通道的类型就是一种只能接收的单向通道。如果此处不进行通道方向约束,一旦外部向通道发送数据,将会造成其他使用到计时器的地方逻辑产生混乱。
C <-chan Time
r runtimeTimer
}
因此,单向通道有利于代码接口的严谨性。