声明类型

声明通道类型
通道本身需要一个类型进行修饰,就像切片类型需要标识元素类型。通道的元素类型就是在其内部传输的数据类型,声明如下:
var 通道变量 chan 通道类型

·通道类型:通道内的数据类型。 ·通道变量:保存通道的变量。 chan类型的空值是nil,声明后需要配合make后才能使用。
通道是引用类型,需要使用make进行创建,格式如下:
通道实例 := make(chan 数据类型)
·数据类型:通道内传输的元素类型。
·通道实例:通过make创建的通道句柄。

例如: ch1 := make(chan int) // 创建一个整型类型的通道

ch2 := make(chan interface{}) // 创建一个空接口类型的通道,可以存放任意格式

type Equip struct{ /* 一些字段 */ }
ch2 := make(chan Equip) // 创建Equip指针类型的通道,可以存放Equip

使用通道发送数据

1.通道发送数据的格式 通道的发送使用特殊的操作符“<-”,将数据通过通道发送的格式为:
通道变量 <- 值
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并没有形成发送和接收对应的代码。
使用通道接收数据 通道接收同样使用“<-”操作符,通道接收有如下特性:
·通道的收发操作在不同的两个goroutine间进行。 由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个goroutine

使用通道接收数据

通道接收同样使用“<-”操作符,通道接收有如下特性:

·通道的收发操作在不同的两个goroutine间进行。 由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个goroutine中进行。

·接收将持续阻塞直到发送方发送数据。 如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。
·每次接收一个元素。 通道一次只能接收一个数据元素。 通道的数据接收一共有以下4种写法。

1.阻塞接收数据

阻塞模式接收数据时,将接收变量作为“<-”操作符的左值,格式如下:

data := <-ch
执行该语句时将会阻塞,直到接收到数据并赋值给data变量。

2.非阻塞接收数据

使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下: data, ok := <-ch
·data:表示接收到的数据。未接收到数据时,data为通道类型的零值。

·ok:表示是否接收到数据。 非阻塞的通道接收方法可能造成高的CPU占用,因此使用非常少。如果需要实现接收超时检测,可以配合select和计时器channel进行,可以参见后面的内容。

3.接收任意数据,忽略接收的数据

阻塞接收数据后,忽略从通道返回的数据,格式如下:

<-ch
执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在goroutine间阻塞收发实现并发同步。

使用通道做并发同步的写法,可以参考下面的例子:

01    package main
02
03 import (
04 "fmt"
05 )
06
07 func main() {
08
09 // 构建一个通道
10 ch := make(chan int)
11
12 // 开启一个并发匿名函数
13 go func() {
14
15 fmt.Println("start goroutine")
16
17 // 通过通道通知main的goroutine
18 ch <- 0
19
20 fmt.Println("exit goroutine")
21
22 }()
23
24 fmt.Println("wait goroutine")
25
26 // 等待匿名goroutine
27 <-ch
28
29 fmt.Println("all done")
30
31 }

代码说明如下:

·第10行,构建一个同步用的通道。 ·第13行,开启一个匿名函数的并发。

·第18行,匿名goroutine即将结束时,通过通道通知main的goroutine,这一句会一直阻塞直到main的goroutine接收为止。

·第27行,开启goroutine后,马上通过通道等待匿名goroutine结束。 执行代码,

输出如下:

wait goroutine
start goroutine
exit goroutine
all done

4.循环接收

通道的数据接收可以借用for range语句进行多个元素的接收操作,格式如下:
for data := range ch {

}
通道ch是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过for遍历获得的变量只有一个,即上面例子中的data。
代码9-1

使用for从通道中接收数据

01    package main
02
03 import (
04 "fmt"
05
06 "time"
07 )
08
09 func main() {
10
11 // 构建一个通道
12 ch := make(chan int)
13
14 // 开启一个并发匿名函数
15 go func() {
16
17 // 从3循环到0
18 for i := 3; i >= 0; i-- {
19
20 // 发送3到0之间的数值
21 ch <- i
22
23 // 每次发送完时等待
24 time.Sleep(time.Second)
25 }
26
27 }()
28
29 // 遍历接收通道数据
30 for data := range ch {
31
32 // 打印通道数据
33 fmt.Println(data)
34
35 // 当遇到数据0时,退出接收循环
36 if data == 0 {
37 break
38 }
39 }
40
41 }

单向通道——通道中的单行道

Go的通道可以在声明时约束其操作方向,如只发送或是只接收。这种被约束方向的通道被称做单向通道。

1.单向通道的声明格式

只能发送的通道类型为chan<-,只能接收的通道类型为<-chan,格式如下:

var 通道实例 chan<- 元素类型 // 只能发送通道
var 通道实例 <-chan 元素类型 // 只能接收通道
·元素类型:通道包含的元素类型。

·通道实例:声明的通道变量。

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<- int)
同理,chRecvOnly也是不能发送的。 当然,使用make创建通道时,也可以创建一个只发送或只读取的通道: ch := make(<-chan int)

var chReadOnly <-chan int = ch
<-chReadOnly
上面代码编译正常,运行也是正确的。但是,一个不能填充数据(发送)只能读取的通道是毫无意义的。

3.time包中的单向通道 time包中的计时器会返回一个timer实例,代码如下: timer := time.NewTimer(time.Second)
timer的Timer类型定义如下:

01    type Timer struct {
02 C <-chan Time
03 r runtimeTimer
04 }

第2行中C通道的类型就是一种只能接收的单向通道。如果此处不进行通道方向约束,一旦外部向通道发送数据,将会造成其他使用到计时器的地方逻辑产生混乱。

带缓冲的通道

在无缓冲通道的基础上,为通道增加一个有限大小的存储空间形成带缓冲通道。带缓冲通道在发送时无需等待接收方接收即可完成发送过程,并且不会发生阻塞,只有当存储空间满时才会发生阻塞。同理,如果缓冲通道中有数据,接收时将不会发生阻塞,直到通道中没有数据可读时,通道将会再度阻塞。 提示:无缓冲通道保证收发过程同步。无缓冲收发过程类似于快递员给你电话让你下楼取快递,整个递交快递的过程是同步发生的,你和快递员不见不散。但这样做快递员就必须等待所有人下楼完成操作后才能完成所有投递工作。如果快递员将快递放入快递柜中,并通知用户来取,快递员和用户就成了异步收发过程,效率可以有明显的提升。带缓冲的通道就是这样的一个“快递柜”。
1.创建带缓冲通道 如何创建带缓冲的通道呢?参见如下代码: 通道实例 := make(chan 通道类型, 缓冲大小)

01    package main
02
03 import "fmt"
04
05 func main() {
06
07 // 创建一个3个元素缓冲大小的整型通道
08 ch := make(chan int, 3)
09
10 // 查看当前通道的大小
11 fmt.Println(len(ch))
12
13 // 发送3个整型元素到通道
14 ch <- 1
15 ch <- 2
16 ch <- 3
17
18 // 查看当前通道的大小
19 fmt.Println(len(ch))
20 }

代码输出如下:

0
3

·第8行,创建一个带有3个元素缓冲大小的整型类型的通道。

·第11行,查看当前通道的大小。带缓冲的通道在创建完成时,内部的元素是空的,因此使用len()获取到的返回值为0。

·第14~16行,发送3个整型元素到通道。因为使用了缓冲通道。即便没有goroutine接收,发送者也不会发生阻塞。

·第19行,由于填充了3个通道,此时的通道长度变为3。

2.阻塞条件
带缓冲通道在很多特性上和无缓冲通道是类似的。无缓冲通道可以看作是长度永远为0的带缓冲通道。因此根据这个特性,带缓冲通道在下面列举的情况下依然会发生阻塞:

(1)带缓冲通道被填满时,尝试再次发送数据时发生阻塞。
(2)带缓冲通道为空时,尝试接收数据时发生阻塞。

通道的多路复用——同时处理接收和发送多个通道的数据

多路复用是通信和网络中的一个专业术语。多路复用通常表示在一个信道上传输多路信号或数据流的过程和技术。 提示:报话机同一时刻只能有一边进行收或者发的单边通信,报话机需要遵守的通信流程如下:
(1)说话方在完成时需要补上一句“完毕”,随后放开通话按钮,从发送切换到接收状态,收听对方说话。 (2)收听方在听到对方说“完毕”时,按下通话按钮,从接收切换到发送状态,开始说话。 电话可以在说话的同时听到对方说话,所以电话是一种多路复用的设备,一条通信线路上可以同时接收或者发送数据。同样的,网线、光纤也都是基于多路复用模式来设计的,网线、光纤不仅可支持同时收发数据,还支持多个人同时收发数据。 在使用通道时,想同时接收多个通道的数据是一件困难的事情。通道在接收数据时,如果没有数据可以接收将会发生阻塞。虽然可以使用如下模式进行遍历,但运行性能会非常差。
for{
// 尝试接收ch1通道
data, ok := <-ch1
// 尝试接收ch2通道
data, ok := <-ch2
// 接收后续通道

}
Go语言中提供了select关键字,可以同时响应多个通道的操作。select的每个case都会对应一个通道的收发过程。当收发完成时,就会触发case中响应的语句。多个操作在每次select中挑选一个进行响应。格式如下: select{
case 操作1:
响应操作1
case 操作2:
响应操作2

default:
没有操作情况
}

模拟rpc

1.客户端请求和接收封装
下面的代码封装了向服务器请求数据,等待服务器返回数据,如果请求方超时,该函数还会处理超时逻辑,详细实现过程请参考代码9-3。
代码9-3 模拟RPC
(具体文件:…/chapter09/rpc/rpc.go)

01        // 模拟RPC客户端的请求和接收消息封装
02 func RPCClient(ch chan string, req string) (string, error) {
03
04 // 向服务器发送请求
05 ch <- req
06
07 // 等待服务器返回
08 select {
09 case ack := <-ch: // 接收到服务器返回数据
10
return ack, nil
11 case <-time.After(time.Second): // 超时
12
return "", errors.New("Time out")
}

·第5行,模拟socket向服务器发送一个字符串信息。服务器接收后,结束阻塞执行下一行。
·第8行,使用select开始做多路复用。注意,select虽然在写法上和switch一样,都可以拥有case和default。但是select关键字后面不接任何语句,而是将要复用的多个通道语句写在每一个case上,如第9行和第11行所示。
·第11行,使用了time包提供的函数After(),从字面意思看就是多少时间之后,其参数是time包的一个常量,time.Second表示1秒。time.After返回一个通道,这个通道在指定时间后,通过通道返回当前时间。
·第12行,在超时时,返回超时错误。

RPCClient()函数中,执行到select语句时,第9行和第11行的通道操作会同时开启。如果第9行的通道先返回,则执行第10行逻辑,表示正常接收到服务器数据;如果第11行的通道先返回,则执行第12行的逻辑,表示请求超时,返回错误。