channel的高级玩法

单向通道

我们在说“通道”时一般说的都是双向通道,即:既可以发也可以收的通道。这里的“发”和“收”是站在操作通道的代码的角度说的。

所谓单向通道,就是只能发不能收,或者只能收不能发的通道。

定义单向通道

var uselessChan = make(chan<- int , 1)  //发送通道:只能发不能收

var uselessChan = make(<-chan int , 1)  //接收通道:只能收不能发

在关键字chan的前面或后面加上通道收发操作符即可表示。

通道就是为了传递数据而存在的,声明一个只有一端能用的通道没有任何意义。

单向通道的用途

概括地讲:单向通道最主要的用途就是约束其他代码的行为。主要在函数中约束代码的行为。

解析

func SendInt(ch chan<- int) {
     ch <- rand.Intn(1000)
}

SendInt函数的参数是一个chan<- int 类型的通道。在这个函数中的代码就只能向参数ch发送元素值,而不能从ch里接收元素值。这就起到了约束函数行为的作用。

在实际场景中:这种约束一般会出现在接口类型声明的某个方法定义上。

type Notifier interface {
     SendInt(ch chan<- int)
}

我们在接口中定义的方法中如果使用了单向通道类型,那么就相当于对这个接口的所有实现做了约束。这个约束方式在编写模板代码或者可扩展的程序库的时候很有用。

我们虽然在方法中声明接收单向通道作为参数,但是,实际向方法传递参数的时候,只需要把一个元素类型匹配的双向通道传递给它就行了,因为Go语言在这种情况下会自动地把双向通道转换为函数所需的单向通道。

我们还可以在函数声明的结果列表中使用单向通道。

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类型的通道,得到该通道的程序,只能从通道中接收元素值。

这是对函数调用方的约束。

带range子句的for语句与通道联用

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

上面的for语句称为带有range子句的for语句。

  • 一、这样一条for语句会不断地尝试从intChan2种取出元素值,即使intChan2被关闭,它也会在取出所有剩余的元素值之后再结束执行。
  • 二、当intChan2中没有元素值时,它会被阻塞在有for关键字的那一行,直到有新的元素值可取。
  • 三、假设intChan2的值为nil,那么它会被永远阻塞在有for关键字的那一行。

select语句与通道联用

select语句只能与通道联用,它一般由若干个分支组成。select语句有2中分支:候选分支,以关键字case开头,后面跟一个case表达式和一个冒号,下一行写要执行的语句;默认分支:以关键字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!")
}
  • 如果没有加默认分支,一旦所有的case表达式都没有满足求值条件,那么select语句就会被阻塞。直到至少有一个case表达式满足条件为止。
  • 加入了默认分支,无论通道操作的表达式是否阻塞,select语句都不会被阻塞。如果那几个表达式都阻塞了,或者说都没有满足求值的条件,那么默认分支就会被选择和执行
  • 前面我们了解到:我们可能会因为通道关闭了,而直接从通道中接收到一个元素类型的零值。所以在很多时候,我们需要通过接收表达式的第二个结果值来判断通道是否已经关闭。一旦发现一个通道关闭了,我们就应该及时屏蔽掉对应的分支或者采取其他措施。
  • select语句只能对其中的每一个case表达式各求值一次。所以,如果我们想连续或定时地操作其中的通道的话,就往往需要通过在for语句中嵌入select语句的方式实现。但这时要注意,简单地在select语句的分支中使用break语句,只能结束当前的select语句的执行,而并不会对外层的for语句产生作用。这种错误的用法可能会让这个for语句无休止地运行下去。

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表达式以及分支中,是否包含并发不安全的代码了

如果在select语句中发现某个通道已关闭,那么应该怎样屏蔽掉它所在的分支?

当第二个boolean参数为false的时候,在相应的case中设置chan为nil零值,再次case求值的时候会遭遇阻塞,会屏蔽该case

在select语句与for语句联用时,怎样直接退出外层的for语句?

通过定义标签,配合goto或者break能实现在同一个函数内任意跳转,故可以跳出多层嵌套的循环。

猜你喜欢

转载自my.oschina.net/u/3672057/blog/2980315