channel的高级玩法

单向通道

咱们在说“通道”时通常说的都是双向通道,即:既能够发也能够收的通道。这里的“发”和“收”是站在操做通道的代码的角度说的。算法

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

定义单向通道

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

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

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

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

单向通道的用途

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

解析

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

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

在实际场景中:这种约束通常会出如今接口类型声明的某个方法定义上。code

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

咱们在接口中定义的方法中若是使用了单向通道类型,那么就至关于对这个接口的全部实现作了约束。这个约束方式在编写模板代码或者可扩展的程序库的时候颇有用。接口

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

咱们还能够在函数声明的结果列表中使用单向通道。get

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语句。

  • 1、这样一条for语句会不断地尝试从intChan2种取出元素值,即便intChan2被关闭,它也会在取出全部剩余的元素值以后再结束执行。
  • 2、当intChan2中没有元素值时,它会被阻塞在有for关键字的那一行,直到有新的元素值可取。
  • 3、假设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能实如今同一个函数内任意跳转,故能够跳出多层嵌套的循环。

相关文章
相关标签/搜索