💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] 单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。 虽然可以使用共享内存(全局变量)进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,很多并发模型中必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。 Go语言采用的并发模型是CSP(Communicating Sequential Processes),提倡 **通过通信共享内存** 而不是通过共享内存而实现通信。 如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。 Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。 ## channel 定义 `channel` 是 Go 语言中一种特有的类型。声明通道类型变量的格式如下: ```go var 变量名称 chan 元素类型 ``` - chan:是关键字 - 元素类型:是指通道中传递元素的类型 示例: ```go var ch1 chan string // 传递字符串的通道 var ch2 chan int // 传递整型的通道 var ch3 chan []string // 传递字符串切片的通道 ``` ## channel 零值 未初始化的通道类型变量其默认零值是nil。 ```go var ch1 chan string // 传递字符串的通道 fmt.Printf("ch1: %v\n", ch1) // 运行结果: // ch1: <nil> ``` ## 初始化 channel 声明的通道类型变量需要使用内置的make函数初始化之后才能使用。具体格式如下: ```go make(chan 元素类型, [缓冲大小]) ``` > channel的缓冲大小是可选的。 示例 ```go ch2 := make(chan int) ch3 := make(chan []string, 1) ``` ## channel 操作 通道共有发送(send)、接收(receive)和关闭(close)三种操作。而发送和接收操作都使用<-符号。 ```go // 定义字符串通道 ch3 := make(chan string, 3) // 发送数据到通道 ch3 <- "jiaxzeng" fmt.Printf("ch3 len is: %v, cap is: %v\n", len(ch3), cap(ch3)) // 接收通道的数据 name := <-ch3 fmt.Printf("name: %v\n", name) fmt.Printf("ch3 len is: %v, cap is: %v\n", len(ch3), cap(ch3)) // 关闭通道 close(ch3) // 关闭通道,再尝试接收 name = <-ch3 fmt.Printf("name: %v\n", name) // 运行结果: // ch3 len is: 1, cap is: 3 // name: jiaxzeng // ch3 len is: 0, cap is: 3 // name: ``` **注意**:一个通道值是可以被垃圾回收掉的。通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。 >[info] 关闭后的通道有以下特点 > 1. 对一个关闭的通道再发送值就会导致 panic。 > 2. 对一个关闭的通道进行接收会一直获取值直到通道为空。 > 3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。 > 4. 关闭一个已经关闭的通道会导致 panic。 ## 无缓冲 channel 无缓冲的通道又称为阻塞的通道。 ```go // 定义字符串通道 ch4 := make(chan string) // 发送数据到通道 ch4 <- "jiaxzeng" // 报错提示 // fatal error: all goroutines are asleep - deadlock! // goroutine 1 [chan send]: // main.main() // /data/code/src/code.jiaxzeng.com/backend/study/9.concurrency/channel.go:35 +0x37 // exit status 2 ``` `deadlock` 表示我们程序中的 goroutine 都被挂起导致程序死锁了。为什么会出现deadlock错误呢? 因为我们使用 `ch4 := make(chan string)` 创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。简单来说就是无缓冲的通道必须有至少一个接收方才能发送成功。 上面的代码会阻塞在`ch4 <- "jiaxzeng"`这一行代码形成死锁,那如何解决这个问题呢? 提前先创建好接收者即可 ```go func chanReceive(c chan string) { name := <-c fmt.Printf("name: %v\n", name) } func main(){ // 定义字符串通道 ch4 := make(chan string) // 接收通道的值 go chanReceive(ch4) // 发送数据到通道 ch4 <- "jiaxzeng" // 关闭通道 close(ch4) } ``` 首先无缓冲通道 ch4 上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时字符串 `jiaxzeng` 才能发送成功,两个 goroutine 将继续执行。相反,如果接收操作先执行,接收方所在的 goroutine 将阻塞,直到 `main goroutine` 中向该通道发送字符串 `jiaxzeng` 。 使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道。 ## 有缓冲 channel 还有另外一种解决上面死锁问题的方法,那就是使用有缓冲区的通道。我们可以在使用 make 函数初始化通道时,可以为其指定通道的容量,例如: ```go func chanReceive(ch chan int) { i := <-ch fmt.Printf("i: %v\n", i) } func main(){ // 定义整型通道 ch5 := make(chan int, 3) // 发送数据 ch5 <- 5 // 关闭通道 close(ch5) // 接收通道的数据 chanReceive(ch5) } // i: 5 ``` 只要通道的容量大于零,那么该通道就属于有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。 我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。 ## 判断通道是否被关闭 当向通道中发送完数据时,我们可以通过close函数来关闭通道。当一个通道被关闭后,再往该通道发送值会引发 `panic` ,从该通道取值的操作会先取完通道中的值。通道内的值被接收完后再对通道执行接收操作得到的值会一直都是对应元素类型的零值。那我们如何判断一个通道是否被关闭了呢? 对一个通道执行接收操作时支持使用如下多返回值模式,基于上面的函数修改... ```go func chanReceive(ch chan int) { // i := <-ch // fmt.Printf("i: %v\n", i) for { // 注意,ok变量只有当通道数据都被接收且关闭才会返回false // 如果发送数据后没有关闭,这里就会发生panic v, ok := <-ch if ok { fmt.Printf("v: %v\n", v) } else { fmt.Println("Read completion") break } } } func main(){ // 定义通道 ch5 := make(chan int, 3) // 发送数据到通道 ch5 <- 5 ch5 <- 10 // 关闭通道 // 先于通道接收,否则会出现panic close(ch5) // 接收通道数据 chanReceive(ch5) } // v: 5 // v: 10 // Read completion ``` ## for range接收值 通常我们会选择使用 for range 循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后会自动退出循环。上面那个示例我们使用 for range 改写后会很简洁。 ```go func chanReceive(ch chan int) { /* for { v, ok := <-ch if ok { fmt.Printf("v: %v\n", v) } else { fmt.Println("Read completion") break } } */ for v := range ch { fmt.Printf("v: %v\n", v) } } ``` >[warning] **注意**:目前Go语言中并没有提供一个不对通道进行读取操作就能判断通道是否被关闭的方法。不能简单的通过len(ch)操作来判断通道是否被关闭。 ## 单向通道 在某些场景下我们可能会将通道作为参数在多个任务函数间进行传递,通常我们会选择在不同的任务函数中对通道的使用进行限制,比如限制通道在某个函数中只能执行发送或只能执行接收操作 单向通道标识符 ```go <- chan int // 只接收通道,只能接收不能发送 chan <- int // 只发送通道,只能发送不能接收 ``` 示例,生产者只使用发送通道,消费者只使用接收通道。 ```go func Producer() <-chan int { ch := make(chan int, 3) go func() { for i := 0; i < 10; i++ { ch <- i } close(ch) }() return ch } func Consumer(ch <-chan int) int { sum := 0 for { v, ok := <-ch if ok { // 通道数据的平方和 sum += v * v } else { break } } return sum } func main() { // Producer函数返回值是只接收的通道 ch6 := Producer() // Consumer函数实参只能是单通道(接收),返回整型类型 sum := Consumer(ch6) fmt.Printf("sum: %v\n", sum) } ``` ## 总结 下面的表格中总结了对不同状态下的通道执行相应操作的结果。 | | nil | 没值 | 有值 | 满 | | :-: | :-: | :-: | :-: | :-: | | 发送 | 阻塞导致死锁 | 成功 | 成功 | 发送阻塞 | | 接收 | 阻塞导致死锁 | 零值 | 成功 | 成功 | | 关闭 | panic | 成功 | 成功 | 成功 |