Go语言核心36讲笔记09--通道的高级玩法

本文详细介绍了 Go 语言中的单向通道及其应用价值,如约束代码行为。同时,文章讲解了 select 语句的基础知识、使用注意点以及分支选择规则,并通过示例代码展示了其工作原理。最后,提出了如何处理关闭的通道和在 for 循环中退出的思考题。
摘要由CSDN通过智能技术生成


1.单向通道

1.1什么是单向通道?

单向通道是只能发不能收或者只能收不能发的通道,一个通道是单向的还是双向的是由它的类型字面量体现的。通道的类型字面量中的<-表示的是通道的方向:

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

1.2单向通道有什么应用价值

单向通道最主要的用途就是约束其他代码的行为,例如:Notifier接口中的SendInt方法只能接受一个发送通道作为参数,在该接口的所有实现类型中的SendInt方法都会受到限制。

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

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

2.select

(1)select基础知识

  • select语句只能与通道联用,每次执行时一般只有一个分支中的代码会被执行;
  • select语句的分支分为两种,分别是候选分支和默认分支,候选分支以关键字case开头,默认分支是defaul case;
  • select语句的每个case表达式中都只能包含操作通道的表达式;

(2)select使用注意点

  • 当select语句中有默认分支时,那么无论是否涉及通道操作的表达式是否有阻塞,select语句都不会被阻塞,如果没有满足求值的条件,那么默认分支就会被选中执行;
  • 如果select语句中没有默认分支,当所有的case表达式都没满足求值条件时select语句就会被阻塞,直到至少有一个case表达式满足条件为止;
  • select语句只能对每一个case表达式各求值一次,所以如果需要连续或者定时操作其中的通道,那么可以通过在for语句中嵌入select语句的方式实现,但此时需要注意简单地在select语句的分支中使用break语句只能结束当前select语句的执行,不会对外层的for语句产生作用;
package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	example1()
	example2()
}

func example1() {
	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!")
	}
}

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

(3)select语句的分支选择规则

  • 对于每一个case表达式,都至少会包含一个代表发送操作的表达式或者一个代表接收操作的接收表达式,同时也可能会包含其他的表达式;
  • select语句包含的候选分支中的case表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下的;
  • 对于每一个case表达式,如果其中的发送表达式或者接受表达式在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值是不成功的;
  • 仅当select语句中的所有case表达式都被求值完成后,它才会开始选择候选分支,这时它只会挑选满足选择条件的候选分支执行,如果所有的候选分支都不满足选择条件,那么默认分支就会被执行;如果没有默认分支,那么select语句就会立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止;
  • 如果select语句中同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行。注意:即使select语句是在被唤醒时发现的这种情况也会这样做;
  • 一个select语句中有且只能有一个默认分支,并且默认分支只在无候选分支可选时才会被执行,这与其编写的位置无关;
  • select语句的每次执行,包括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]
}

3.思考题

(1)如果在select语句中发现某个通道已关闭,那么应该怎样屏蔽掉它所在的分支?
答:判断该通道是否已关闭,如果关闭了则将其重新赋值为nil,此外在select语句中搭配default使用则会保证select语句不会被阻塞

select {
    case _, ok := <-ch1:
        if !ok {
            ch1 = nil
        }
    case ... :
    ...
    default:
    ...
    }
}

(2)在select语句与for语句联用时,怎样直接退出外层的for语句?
答:有两种方式,方式一:使用break搭配标签方式,break到指定的循环体;方式二:使用goto语句跳转到指定标签处;

package main

import (
	"fmt"
	"time"
)

func main() {
	example1()
	example2()
}

func example1() {
	ch1 := make(chan int, 1)
	time.AfterFunc(time.Second, func() { close(ch1) })
loop:
	for {
		select {
		case _, ok := <-ch1:
			if !ok {
				break loop
			}
			fmt.Println("in")
		}
	}
	fmt.Println("out")
}

func example2() {
	ch1 := make(chan int, 1)
	time.AfterFunc(time.Second, func() { close(ch1) })

	for {
		select {
		case _, ok := <-ch1:
			if !ok {
				goto loop
			}
			fmt.Println("in")
		}
	}
loop:
	fmt.Println("out")
}

本节示例代码地址:demo Github

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值