单向通道
声明+初始化
var uselessChan = make(chan<- int, 1)
func test(ch chan<- int)
chan<- int类型:channel中的元素类型时int,单向通道,当前函数test只能向channel发送,即,这是一个发送通道。
与发送操作和接收操作对应,这里的“发”和“收”都是在操作通道的代码(func)的角度上说的。 问题:单向通道有什么应用价值?
典型回答
概括地说,单向通道最主要的用途就是约束其他代码的行为。
问题解析
这需要从两个方面讲,都跟函数的声明有些关系。
- 声明中的函数参数
func SendInt(ch chan<- int) { ch <- rand.Intn(1000) }
用
func
关键字声明了一个叫做SendInt
的函数。这个函数只接受一个chan<- int
类型的参数。在这个函数中的代码只能向参数ch
发送元素值,而不能从它那里接收元素值。这就起到了约束函数行为的作用。👆这个例子比较理想化。经典的约束体现在interface内函数的参数,可以影响后面的所有实现类型。
在实际场景中,这种约束一般会出现在接口类型声明中的某个方法定义上。请看这个叫
Notifier
的接口类型声明:
type Notifier interface { SendInt(ch chan<- int) }
在接口类型声明的花括号中,每一行都代表着一个方法的定义。接口中的方法定义与函数声明很类似,但是只包含了方法名称、参数列表和结果列表。
一个类型如果想成为一个接口类型的实现类型,那么就必须实现这个接口中定义的所有方法。因此,如果我们在某个接口内方法的定义中使用了单向通道类型,那么就相当于在对它的所有实现做出约束。
👆.go中,
Notifier
接口中的SendInt
方法只会接受一个发送通道作为参数,所以,在该接口的所有实现类型中的SendInt
方法都会受到限制。这种约束方式还是很有用的,尤其是在我们编写模板代码或者可扩展的程序库的时候。
Tips
调用
SendInt
函数的时候(👇.go),只需要把一个元素类型匹配的双向通道传给它就行了,因为 Go 语言在这种情况下会自动地把双向通道转换为函数所需的单向通道。
intChan1 := make(chan int, 3) SendInt(intChan1)
- 函数声明的结果列表为单向channel
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
类型的通道,这就意味着得到该通道的程序,只能从通道中接收元素值。调用函数
getIntChan
举例
intChan2 := getIntChan() for elem := range intChan2 { fmt.Printf("The element in intChan2: %v\n", elem) }
关于迭代使用channel中元素的两种方式:带range的for语句;select语句(专门为了操作通道而存在)。
对第一种方式的使用说明
- 一、这样一条
for
语句会不断地尝试从intChan2
种取出元素值,即使intChan2
被关闭,它也会在取出所有剩余的元素值之后再结束执行。- 二、当
intChan2
中没有元素值时,它会被阻塞在有for
关键字的那一行,直到有新的元素值可取。- 三、若
intChan2
的值为nil
,那么它会被永远阻塞在有for
关键字的那一行。
知识扩展
问题 1:select
语句与通道怎样联用,应该注意些什么?
- select语句只能与channel连用且每个表达式中都只能操作包含操作通道的表达式,比如接收表达式。如果我们需要把接收表达式的结果赋给变量的话,还可以把这里写成赋值语句或者短变量声明。
- 一次只会选择其中一条分支的代码执行。
- 两个分支类型,case xxx :|| default: 举例子
// 准备好几个通道,放在通道数组里。
intChannels := [3]chan int{ //通道数组的短变量声明+初始化方式
make(chan int, 1), //类型为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!")
}
几点总结:
select语句加入
default:
后,无论channel是否阻塞,该代码块都不会阻塞。接👆讨论,若无
default:
,当所有case都不满足求值条件时,select语句会被阻塞,直到其中一个case满足条件。当某通道关闭了,
select
语句会直接从通道接收到一个其元素类型的零值,并且每次循环都会执行该case,当select
中只有这个case
时,会出现死循环。所以,在很多时候,我们需要通过接收表达式的第二个结果值来判断通道是否已经关闭。一旦发现某个通道关闭了,应该及时地屏蔽掉对应的分支或者采取其他措施。
select
语句只能对其中的每一个case
表达式各求值一次。所以,如果我们想连续或定时地操作其中的通道的话,就往往需要通过在for
语句中嵌入select
语句的方式实现。but,简单地在select
语句的分支中使用break
语句,只能结束当前的select
语句的执行,而并不会对外层的for
语句产生作用。容易引起死循环。
intChan := make(chan int, 1) //类型为 chan int,容量为1 // 一秒后关闭通道。 time.AfterFunc(time.Second, func() { close(intChan) }) select { case _, ok := <-intChan: //利用接收表达式的第二个值来判断channel是否关闭 if !ok { fmt.Println("The candidate case is closed.") break //直接终止select执行 } fmt.Println("The candidate case is selected.") }
问题 2:select
语句的分支选择规则都有哪些?
规则描述(结合👇的demo看)
对于每一个
case
表达式,都至少会包含一个代表发送操作的发送表达式或者一个代表接收操作的接收表达式,同时也可能会包含其他的表达式。比如,如果case
表达式是包含了接收表达式的短变量声明时,那么在赋值符号左边的就可以是一个或两个表达式,不过此处的表达式的结果必须是可以被赋值的。当这样的case
表达式被求值时,它包含的多个表达式总会以从左到右的顺序被求值。
select
语句包含的候选分支中的case
表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下的。结合上一条规则,在select
语句开始执行时,排在最上边的候选分支中最左边的表达式会最先被求值,然后是它右边的表达式。仅当最上边的候选分支中的所有表达式都被求值完毕后,从上边数第二个候选分支中的表达式才会被求值,顺序同样是从左到右,然后是第三个候选分支、第四个候选分支,以此类推。对于每一个
case
表达式,如果其中的发送表达式或者接收表达式在被求值时,相应的操作正处于阻塞状态,那么对该case
表达式的求值就是不成功的。在这种情况下,我们可以说,这个case
表达式所在的候选分支是不满足选择条件的。仅当
select
语句中的所有case
表达式都被求值完毕后,它才会开始选择候选分支。这时候,它只会挑选满足选择条件的候选分支执行。如果所有的候选分支都不满足选择条件,那么默认分支就会被执行。如果这时没有默认分支,那么select
语句就会立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止。一旦有一个候选分支满足选择条件,select
语句(或者说它所在的 goroutine)就会被唤醒,这个候选分支就会被执行。如果
select
语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行。注意,即使select
语句是在被唤醒时发现的这种情况,也会这样做。一条
select
语句中只能够有一个默认分支。并且,默认分支只在无候选分支可选时才会被执行,这与它的编写位置无关。
select
语句的每次执行,包括case
表达式求值和分支选择,都是独立的。不过,至于它的执行是否是并发安全的,就要看其中的case
表达式以及分支中,是否包含并发不安全的代码了。
package main
import "fmt"
var channels = [3]chan int{
nil,
make(chan int),
nil,
}
var numbers = []int{1, 2, 3}
func main() {
select {
case getChan(0) <- getNumber(0):
fmt.Println("The first candidate case is selected.")
case getChan(1) <- getNumber(1):
fmt.Println("The second candidate case is selected.")
case getChan(2) <- getNumber(2):
fmt.Println("The third candidate case is selected")
default:
fmt.Println("No candidate case is selected!")
}
}
func getNumber(i int) int {
fmt.Printf("numbers[%d]\n", i)
return numbers[i]
}
func getChan(i int) chan int {
fmt.Printf("channels[%d]\n", i)
return channels[i]
}
总结
单向通道的表示方法,操作符“
<-
”。概括单向通道存在的意义的话,是“约束”,对代码的约束。我们可以使用带
range
子句的for
语句从通道中获取数据,也可以通过select
语句操纵通道。
select
语句是专门为通道而设计的,它可以包含若干个候选分支,每个分支中的case
表达式都会包含针对某个通道的发送或接收操作。当
select
语句被执行时,它会根据一套分支选择规则选中某一个分支并执行其中的代码。如果所有的候选分支都没有被选中,那么默认分支(如果有的话)就会被执行。注意,发送和接收操作的阻塞是分支选择规则的一个很重要的依据。
思考题
- 如果在
select
语句中发现某个通道已关闭,那么应该怎样屏蔽掉它所在的分支?
很简单,把nil赋给代表了这个通道的变量就可以了。如此一来,对于这个通道(那个变量)的发送操作和接收操作就会永远被阻塞。
- 在
select
语句与for
语句联用时,怎样直接退出外层的for
语句?
这一般会用到goto语句和标签(label),具体请参看 Go 语言规范的这部分。