1.通道的知识点
- 可以利用通道在多个gotoutine之间传递数据
- 通道类型的值本身就是并发安全的
- 在声明并初始化一个通道时需要用到go语言的内建函数make,该函数需要传递两个参数,一个是通道类型的元素类型。另一个是通道的容量,该参数是可选的,所谓通道的容量,就是指通道最多可以缓存多少个元素值。根据通道容量的大小是否大于0可以将通道分为两种类型:非缓冲通道和缓冲通道,非缓冲通道的容量为0,缓冲通道的容量大于0
ch1 := make(chan int, 1)
- 一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符<-。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。
2.对通道的发送和接收都有哪些基本特性?
- 在同一时刻,Go 语言的运行时系统只会执行对同一个通道的任意个发送操作中的某一个。直到这个元素值被完全复制进该通道之后,其他针对该通道的发送操作才可能被执行。同样的,在同一时刻,运行时系统也只会执行,对同一个通道的任意个接收操作中的某一个。直到这个元素值完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。即使这些操作是并发执行的也是如此。
- 对于通道中的同一个元素值来说,发送操作和接收操作之间也是互斥的
- 元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。
- 元素值从通道进入外界时会被移动。这个移动操作实际上包含了两步,第一步是生成 正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。
- 对于通道中的同一个 元素值来说,它只可能是某一个发送操作放入的,同时也只可能被某一个接收操作取出
- 一般情况下,发送操作包括了“复制元素值”和“放置副本到通道内 部”这两个步骤。在这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。接收操作通常包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步 骤。在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的 goroutine 收到了运行时系统的通知并重新获得运行机会为止。
3.发送操作和接收操作在什么时候可能被长时间的阻塞?
- 对于缓冲通道而言,如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有 元素值被接收走。这时,通道会优先通知最早因此而等待的、那个发送操作所在的 goroutine,后者会再次执行发 送操作。由于发送操作在这种情况下被阻塞后,它们所在的 goroutine 会顺序地进入通道内部的发送等 待队列,所以通知的顺序总是公平的。相对的,如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。 这时,通道会通知最早等待的那个接收操作所在的 goroutine,并使它再次执行接收操作。因此而等待的、所有接收操作所在的goroutine,都会按照先后顺序被放入通道内部的接收等待队列。
- 对于非缓冲通道而言,无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。 也就是说,只有收发双方对接上了,数据才会被传递。并且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。而缓冲通道则在用异步的方式传递数据。
- 对于值为nil的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行。
注意:
- 在大多数情况下,缓冲通道会作为收发双方的中间件。元素值会先从发送方复制到缓冲通道,之后再由缓冲通道复制给接收方。但是,当发送操作在执行的时候发现空的通道中,正好有等待的接收操作,那么它会直接把元素 值复制给接收方。
- 由于通道类型是引用类型,所以它的零值就是nil。换句话说,当我们只声明该类型的变 量但没有用make函数对它进行初始化时,该变量的值就会是nil
4.发送操作和接收操作在什么时候会引发 panic?
- 对于一个已初始化,但并未关闭的通道来说,收发操作一定不会引发 panic。但是通道一旦关闭,再对它进行发送操作,就会引发 panic。
- 如果我们试图关闭一个已经关闭了的通道,也会引发 panic。注意,接收操作是可以感知 到通道的关闭的,并能够安全退出。
- 如果通道关闭时,里面还有元素值未被取出,那么接收表达式的第一个结果,仍会是通道中的某一个元素值,而第二个结果值一定会是true。因此,通过接收表达式的第二个结果值,来判断通道是否关闭是可能有延时的。
elem, ok := <-ch1
5.思考题
(1)通道的长度代表着什么?它在什么时候会通道的容量相同?
答:通道的长度是其内部缓冲队列未读的数据量,而通道的容量是缓冲区可最大盛放的数据量,可以使用 len 函数去计算通道的长度,使用 cap 函数去获得通道的容量;当通道已放置的元素数量达到通道的容量时,通道的长度和容量相等。
(2)元素值在经过通道传递时会被复制,那么这个复制是浅表复制还是深层复制呢?
答:浅表拷贝,具体见一下测试demo
package main
import "fmt"
type Employee struct {
Name string
Age int
}
func main() {
ch1 := make(chan []int, 1)
s1 := []int{1, 2, 3}
ch1 <- s1
s2 := <-ch1
s1[0] = 100
fmt.Println(s1, s2) //[100 2 3] [100 2 3]
s2[1] = 100
fmt.Println(s1, s2) //[100 100 3] [100 100 3]
ch2 := make(chan [3]int, 1)
s3 := [3]int{1, 2, 3}
ch2 <- s3
s4 := <-ch2
s4[0] = 100
fmt.Println(s3, s4) //[100 2 3] [1 2 3]
employee1 := Employee{"sunxi", 18}
ch3 := make(chan Employee, 1)
ch3 <- employee1
employee2 := <-ch3
fmt.Printf("%p\n", &employee1)
fmt.Printf("%p\n", &employee2)
ch4 := make(chan *int, 2)
i := 1
fmt.Println("i: ", &i, i)
ch4 <- &i
p := <-ch4
fmt.Println("p: ", p, *p)
i = 2
fmt.Println("i: ", &i, i)
}
本节示例代码地址:demo GitHub