Go进阶5 Channel的高级应用

570 阅读7分钟

单向通道与双向通道
通常我们创建的通道都是双向的,也就是既能接收也能发送元素,而单项通道则代表只能接收元素或者发送元素的通道。

单向通道的使用
在日常开发中,我们可以使用单向通道对其他代码进行规范约束。

单向通道声明代码实例

var uselessChan = make(chan<- int, 1)

var uselessChan = make(<-chan int, 1)

使用单向通道约束接口行为

//声明一个接口,约束SendInt的入参为单项发送通道
type Publisher interface {
    SendInt(ch chan<- int)//对该接口的所有实现做出约束。
}

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
}

遍历通道元素值的方式
带range子句的for语句 + Channel

  1. for语句会不断地尝试从intChan2种取出元素值,即使intChan2被关闭,它也会在取出所有剩余的元素值之后再结束执行。
  2. 当intChan2中没有元素值时,它会被阻塞在有for关键字的那一行,直到有新的元素值可取。
  3. 假设intChan2的值为nil,那么它会被永远阻塞在有for关键字的那一行。
intChan2 := getIntChan()

for elem := range intChan2 {
    fmt.Printf("The element in intChan2: %v\n", elem)
}

Select语句的使用
select语句只能与通道联用,它一般由若干个分支组成。每次执行这种语句的时候,一般只有一个分支中的代码会被运行。select语句是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达式,比如接收表达式。

候选分支与默认分支
select语句的分支分为两种,一种叫做候选分支,另一种叫做默认分支。

  1. 候选分支总是以关键字case开头,后跟一个case表达式和一个冒号,然后我们可以从下一行开始写入当分支被选中时需要执行的语句。

  2. 默认分支其实就是 default case,因为,当且仅当没有候选分支被选中时它才会被执行,所以它以关键字default开头并直接后跟一个冒号。同样的,我们可以在default:的下一行写入要执行的语句。

Select分支的选择原则

  1. 对于每一个case表达式,都至少会包含一个代表发送操作的发送表达式或者一个代表接收操作的接收表达式,同时也可能会包含其他的表达式。比如,如果case表达式是包含了接收表达式的短变量声明时,那么在赋值符号左边的就可以是一个或两个表达式,不过此处的表达式的结果必须是可以被赋值的。当这样的case表达式被求值时,它包含的多个表达式总会以从左到右的顺序被求值。

  2. select语句包含的候选分支中的case表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下的。结合上一条规则,在select语句开始执行时,排在最上边的候选分支中最左边的表达式会最先被求值,然后是它右边的表达式。仅当最上边的候选分支中的所有表达式都被求值完毕后,从上边数第二个候选分支中的表达式才会被求值,顺序同样是从左到右,然后是第三个候选分支、第四个候选分支,以此类推。

  3. 对于每一个case表达式,如果其中的发送表达式或者接收表达式在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值就是不成功的。在这种情况下,我们可以说,这个case表达式所在的候选分支是不满足选择条件的。

  4. 仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。这时候,它只会挑选满足选择条件的候选分支执行。如果所有的候选分支都不满足选择条件,那么默认分支就会被执行。如果这时没有默认分支,那么select语句就会立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止。一旦有一个候选分支满足选择条件,select语句(或者说它所在的 goroutine)就会被唤醒,这个候选分支就会被执行。

  5. 如果select语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行。注意,即使select语句是在被唤醒时发现的这种情况,也会这样做。

  6. 一条select语句中只能够有一个默认分支。并且,默认分支只在无候选分支可选时才会被执行,这与它的编写位置无关

  7. select语句的每次执行,包括case表达式求值和分支选择,都是独立的。不过,至于它的执行是否是并发安全的,就要看其中的case表达式以及分支中,是否包含并发不安全的代码了。

代码实例

package main

import (
    "fmt"
    "math/rand"
)

func main() {
    
// 准备好几个通道。

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!")

}

}

Result:
The index: 2
The third candidate case is selected, the element is 2.    

使用Select分支的注意事项

  1. 如果加入了默认分支,那么无论涉及通道操作的表达式是否有阻塞select语句都不会被阻塞。如果那几个表达式都阻塞了,或者说都没有满足求值的条件,那么默认分支就会被选中并执行。

  2. 如果没有加入默认分支,那么一旦所有的case表达式都没有满足求值条件,那么select语句就会被阻塞。直到至少有一个case表达式满足条件为止。

  3. 在很多时候,我们需要通过接收表达式的第二个结果值来判断通道是否已经关闭。一旦发现某个通道关闭了,我们就应该及时地屏蔽掉对应的分支或者采取其他措施。这对于程序逻辑和程序性能都是有好处的。

  4. select语句只能对其中的每一个case表达式各求值一次。所以,如果我们想连续或定时地操作其中的通道的话,就往往需要通过在for语句中嵌入select语句的方式实现。但这时要注意,简单地在select语句的分支中使用break语句,只能结束当前的select语句的执行,而并不会对外层的for语句产生作用。这种错误的用法可能会让这个for语句无休止地运行下去。

通道关闭状态判断处理代码实例

package main

import (
    "fmt"
    "time"
)

func main() {
    
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.")
}

}

Result:
The candidate case is closed.