标签:整理 等于 传递 考题 after 输出 价值 利用 因此
我们已经讨论过了通道的基本操作以及背后的规则。今天,我再来讲讲通道的高级玩法。
首先来说说单向通道。我们在说“通道”的时候指的都是双向通道,即:既可以发也可以收的通道。
所谓单向通道就是,只能发不能收,或者只能收不能发的通道。一个通道是双向的,还是单向的是由它的类型字面量体现的。
还记得我们在上篇文章中说过的接收操作符<-吗?如果我们把它用在通道的类型字面量中,那么它代表的就不是“发送”或“接收”的动作了,而是表示通道的方向。
比如:
var uselessChan = make(chan<- int, 1)
我声明并初始化了一个名叫uselessChan的变量。这个变量的类型是chan<- int,容量是1。
请注意紧挨在关键字chan右边的那个<-,这表示了这个通道是单向的,并且只能发而不能收。
类似的,如果这个操作符紧挨在chan的左边,那么就说明该通道只能收不能发。所以,前者可以被简称为发送通道,后者可以被简称为接收通道。
注意,与发送操作和接收操作对应,这里的“发”和“收”都是站在操作通道的代码的角度上说的。
从上述变量的名字上你也能猜到,这样的通道是没用的。通道就是为了传递数据而存在的,声明一个只有一端(发送端或者接收端)能用的通道没有任何意义。那么,单向通道的用途究竟在哪儿呢?
问题:单向通道有什么应用价值?
你可以先自己想想,然后再接着往下看。
典型回答
概括地说,单向通道最主要的用途就是约束其他代码的行为。
问题解析
这需要从两个方面讲,都跟函数的声明有些关系。先来看下面的代码:
func SendInt(ch chan<- int) { ch <- rand.Intn(1000) }
我用func关键字声明了一个叫做SendInt的函数。这个函数只接受一个chan<- int类型的参数。在这个函数中的代码只能向参数ch发送元素值,而不能从它那里接收元素值。这就起到了约束函数行为的作用。
你可能会问,我自己写的函数自己肯定能确定操作通道的方式,为什么还要再约束?好吧,这个例子可能过于简单了。在实际场景中,这种约束一般会出现在接口类型声明中的某个方法定义上。请看这个叫Notifier的接口类型声明:
type Notifier interface { SendInt(ch chan<- int) }
在接口类型声明的花括号中,每一行都代表着一个方法的定义。接口中的方法定义与函数声明很类似,但是只包含了方法名称、参数列表和结果列表。
一个类型如果想成为一个接口类型的实现类型,那么就必须实现这个接口中定义的所有方法。因此,如果我们在某个方法的定义中使用了单向通道类型,那么就相当于在对它的所有实现做出约束。
在这里,Notifier接口中的SendInt方法只会接受一个发送通道作为参数,所以,在该接口的所有实现类型中的SendInt方法都会受到限制。这种约束方式还是很有用的,尤其是在我们编写模板代码或者可扩展的程序库的时候。
顺便说一下,我们在调用SendInt函数的时候,只需要把一个元素类型匹配的双向通道传给它就行了,没必要用发送通道,因为 Go 语言在这种情况下会自动地把双向通道转换为函数所需的单向通道。
intChan1 := make(chan int, 3) SendInt(intChan1)
在另一个方面,我们还可以在函数声明的结果列表中使用单向通道。如下所示:
func getIntChan() <-chan int { num := 5 ch := make(chan int, num) for i := 0; i < num; i++ { ch <- i } close(ch) return ch }
函数getIntChan会返回一个<-chan int类型的通道,这就意味着得到该通道的程序,只能从通道中接收元素值。这实际上就是对函数调用方的一种约束了。
另外,我们在 Go 语言中还可以声明函数类型,如果我们在函数类型中使用了单向通道,那么就相等于在约束所有实现了这个函数类型的函数。
我们再顺便看一下调用getIntChan的代码:
intChan2 := getIntChan() for elem := range intChan2 { fmt.Printf("The element in intChan2: %v\n", elem) }
我把调用getIntChan得到的结果值赋给了变量intChan2,然后用for语句循环地取出了该通道中的所有元素值,并打印出来。
这里的for语句也可以被称为带有range子句的for语句。它的用法我在后面讲for语句的时候专门说明。现在你只需要知道关于它的三件事:
这就是带range子句的for语句与通道的联用方式。不过,它是一种用途比较广泛的语句,还可以被用来从其他一些类型的值中获取元素。除此之外,Go 语言还有一种专门为了操作通道而存在的语句:select语句。
知识扩展
问题 1:select语句与通道怎样联用,应该注意些什么?
select语句只能与通道联用,它一般由若干个分支组成。每次执行这种语句的时候,一般只有一个分支中的代码会被运行。
select语句的分支分为两种,一种叫做候选分支,另一种叫做默认分支。候选分支总是以关键字case开头,后跟一个case表达式和一个冒号,然后我们可以从下一行开始写入当分支被选中时需要执行的语句。
默认分支其实就是 default case,因为,当且仅当没有候选分支被选中时它才会被执行,所以它以关键字default开头并直接后跟一个冒号。同样的,我们可以在default:的下一行写入要执行的语句。
由于select语句是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达式,比如接收表达式。
当然,如果我们需要把接收表达式的结果赋给变量的话,还可以把这里写成赋值语句或者短变量声明。下面展示一个简单的例子。
// 准备好几个通道。 intChannels := [3]chan int{ make(chan int, 1), make(chan int, 1), make(chan int, 1), } // 随机选择一个通道,并向它发送元素值。 index := rand.Intn(3) fmt.Printf("The index: %d\n", index) intChannels[index] <- index // 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。 select { case <-intChannels[0]: fmt.Println("The first candidate case is selected.") case <-intChannels[1]: fmt.Println("The second candidate case is selected.") case elem := <-intChannels[2]: fmt.Printf("The third candidate case is selected, the element is %d.\n", elem) default: fmt.Println("No candidate case is selected!") }
我先准备好了三个类型为chan int、容量为1的通道,并把它们存入了一个叫做intChannels的数组。
然后,我随机选择一个范围在[0, 2]的整数,把它作为索引在上述数组中选择一个通道,并向其中发送一个元素值。
最后,我用一个包含了三个候选分支的select语句,分别尝试从上述三个通道中接收元素值,哪一个通道中有值,哪一个对应的候选分支就会被执行。后面还有一个默认分支,不过在这里它是不可能被选中的。
在使用select语句的时候,我们首先需要注意下面几个事情。
下面是一个简单的示例。
intChan := make(chan int, 1) // 一秒后关闭通道。 time.AfterFunc(time.Second, func() { close(intChan) }) select { case _, ok := <-intChan: if !ok { fmt.Println("The candidate case is closed.") break } fmt.Println("The candidate case is selected.") }
我先声明并初始化了一个叫做intChan的通道,然后通过time包中的AfterFunc函数约定在一秒钟之后关闭该通道。
后面的select语句只有一个候选分支,我在其中利用接收表达式的第二个结果值对intChan通道是否已关闭做了判断,并在得到肯定结果后,通过break语句立即结束当前select语句的执行。
这个例子以及前面那个例子都可以在 demo24.go 文件中被找到。你应该运行下,看看结果如何。
上面这些注意事项中的一部分涉及到了select语句的分支选择规则。我觉得很有必要再专门整理和总结一下这些规则。
问题 2:select语句的分支选择规则都有哪些?
规则如下面所示。
我把与以上规则相关的示例放在 demo25.go 文件中了。你一定要去试运行一下,然后尝试用上面的规则去解释它的输出内容。
总结
今天,我们先讲了单向通道的表示方法,操作符“<-”仍然是关键。如果只用一个词来概括单向通道存在的意义的话,那就是“约束”,也就是对代码的约束。
我们可以使用带range子句的for语句从通道中获取数据,也可以通过select语句操纵通道。
select语句是专门为通道而设计的,它可以包含若干个候选分支,每个分支中的case表达式都会包含针对某个通道的发送或接收操作。
当select语句被执行时,它会根据一套分支选择规则选中某一个分支并执行其中的代码。如果所有的候选分支都没有被选中,那么默认分支(如果有的话)就会被执行。注意,发送和接收操作的阻塞是分支选择规则的一个很重要的依据。
思考题
今天的思考题都由上述内容中的线索延伸而来。
标签:整理 等于 传递 考题 after 输出 价值 利用 因此
原文地址:https://www.cnblogs.com/peter-yan/p/14394377.html