对通道的发送和接收操作都有哪些基本的特性?
- 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。
- 发送操作和接收操作中对元素值的处理都是不可分割的。
- 发送操作在完全完成之前会被阻塞。接收操作也是如此。
第一个基本特性。在同一时刻,Go 语言的运行时系统(以下简称运行时系统)只会执行对同一个通道的任意个发送操作或接受操作中的某一个。
其中注意的一个细节是,元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。
第二个基本特性。 这里的“不可分割”的意思是,它们处理元素值时都是一气呵成的,绝不会被打断。原子性。
第三个基本特性。 一般情况下,发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤。另外,接收操作通常包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步骤。
发送操作和接收操作在什么时候可能被长时间的阻塞?
- 向 nil channel 写数据将卡死,一直阻塞
- 从 nil channel 读数据将卡死,一直阻塞
- 向已满的缓存 channel 写数据将卡死,一直阻塞
- 向已空的缓存 channel 读数据将卡死,一直阻塞
- 向非缓存 channel 读写数据,一开始执行就会被阻塞,直到配对的操作也开始执行
配对操作
package main
import "sync"
func main() {
// wg 用来等待程序完成
// 计数加 2,表示要等待两个goroutine
var wg sync.WaitGroup
wg.Add(2)
c := make(chan int)
go func(){
// 在函数退出时调用Done 来通知main 函数工作已经完成
defer wg.Done()
c <- 520 // 流入信道,堵塞当前线, 没人取走数据信道不会打开
}()
go func(){
// 在函数退出时调用Done 来通知main 函数工作已经完成
defer wg.Done()
println(<-c) // 接受数据,阻塞解除
}()
wg.Wait()
}
发送操作和接收操作在什么时候会引发 panic?
- 向已关闭的 channel 写数据将造成 panic
- 向已关闭的 channel 再次关闭将会造成panic
通过接收表达式的第二个结果值,来判断通道是否关闭是可能有延时的,应当让发送方来关闭通道。
如果通道关闭时,里面还有元素值未被取出,那么接收表达式的第一个结果,仍会是通道中的某一个元素值,而第二个结果值一定会是true
package main
import "fmt"
func main() {
ch1 := make(chan int, 2)
// 发送方。
go func() {
for i := 0; i < 10; i++ {
fmt.Printf("Sender: sending element %v...\n", i)
ch1 <- i
}
fmt.Println("Sender: close the channel...")
close(ch1)
}()
// 接收方。
for {
elem, ok := <-ch1
if !ok {
fmt.Println("Receiver: closed channel")
break
}
fmt.Printf("Receiver: received an element: %v\n", elem)
}
fmt.Println("End.")
}
通道的长度代表着什么?它在什么时候会和通道的容量相同?
长度代表通道当前包含的元素个数,容量就是初始化时你设置的那个数。
什么是单向通道?
我们在说“通道”的时候指的都是双向通道,即:既可以发也可以收的通道。
所谓单向通道就是,只能发不能收,或者只能收不能发的通道。一个通道是双向的,还是单向的是由它的类型字面量体现的。
var uselessChan = make(chan<- int, 1)
//发送通道,操作符<-在关键字chan右边,表示对操作管道来说,只能发而不能收
var uselessChan = make(<-chan int, 1)
//接受通道,操作符<-在关键字chan左边,表示对操作管道来说,只能收而不能发
intChan1 := make(chan int, 3)
SendInt(intChan1)
//双向通道,
从上述变量的名字上你也能猜到,这样的通道是没用的。通道就是为了传递数据而存在的,声明一个只有一端(发送端或者接收端)能用的通道没有任何意义。
单向通道的用途究竟在哪儿呢?
概括地说,单向通道最主要的用途就是约束其他代码的行为,更大作用于编写模板代码或者可扩展的程序库的时候。
func SendInt(ch chan<- int) {
//只能接受,不能发送
ch <- rand.Intn(1000)
}
实际场景中,这种约束一般会出现在接口类型声明中的某个方法定义上
type Notifier interface {
SendInt(ch chan<- int)
}
调用有以单向通道作为参的函数时,只需要传入元素类型匹配的双向通道,会自动地把双向通道转换为函数所需的单向通道。
select语句与通道怎样联用,应该注意些什么?
- select语句只能与通道联用,它一般由若干个分支组成。每次执行这种语句的时候,一般只有一个分支中的代码会被运行。
- 基本功能与switch差不多,只不过select case 表达式中都只能包含操作通道的表达式,比如接收表达式。
注意
因从已关闭的通道接受只能接受到nil,所以通常通过接受表达式的第二个结果值来判断通道是否已经关闭
select语句只能对其中的每一个case表达式各求值一次。
所以,如果我们想连续或定时地操作其中的通道的话,就往往需要通过在for语句中嵌入select语句的方式实现。
select语句的分支选择规则都有哪些?
- 每个case表达式,都至少会包含一个代表操作的可以被赋值表达式。
- case表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下的。
- 对于每一个case表达式,如果其中的发送表达式或者接收表达式在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值就是不成功的。
- 仅当select语句中的所有case表达式都被求值完毕后,它才开始选择候选分支。
- 如果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]
}
如果在select语句中发现某个通道已关闭,那么应该怎样屏蔽掉它所在的分支?
在候选分支中将接收表达式赋值给两个变量,使用第二个变量来判断 channel 是否关闭,如果关闭,可以使用 break 来屏蔽这条候选分支的逻辑代码。
在select语句与for语句联用时,怎样直接退出外层的for语句?
显然使用 break 语句只能推出 select 分支语句,无法退出外层 for 循环,要想在 select 分支语句中直接跳出外层循环,需要借助 标签来完成,在外层 for 循环语句前 定义标签 Loop:(当然也可以起个其他名字)在 select 的 case 语句中使用 goto Loop 或者 break Loop 来直接跳出外层 for 循环。
当然,这两者是有区别的。区别在于「break 标签 」只能用于 for 循环,且标签在 for 循环前面。而「goto 标签」是一个通用的跳转语句。
package main
import "fmt"
func main() {
c := make(chan int, 2)
cc := make(chan int, 1)
c <- 1
c <- 2
cc <- 3
Loop:
for {
select {
case <-c:
fmt.Print("c 1")
case <-c:
fmt.Print("c 2")
goto end
case <-cc:
break Loop
}
}
fmt.Print("out for")
end:
fmt.Print("out end")
}