Golang并发通信机制Channel通道详细说明并不只看表面做法,关键还要理解相关条件、限制和后续影响。
上期内容为大家带来了Go语言goroutine协程的知识点学习,这期内容我将为大家带来Go语言中核心的并发通信机制——Channel通道的学习,这部分内容是实现goroutine之间安全通信的关键,也是Go语言“不要通过共享内存来通信,而要通过通信来共享内存”设计理念的核心体现。
Channel(通道)是Go语言中各个并发执行体(goroutine)之间的通信机制,我们可以把它理解成goroutine之间通信的“管道”,就像生活中水管传递水一样,Channel可以在goroutine之间传递数据。
Channel是类型相关的,也就是说一个Channel只能传递指定类型的数据,比如传递int类型的Channel就不能传递string类型的数据。并且Channel的核心作用是在多个goroutine之间传递数据和同步执行,解决多goroutine并发访问数据的安全问题。
在Go语言中,定义Channel的语法格式如下:
var 通道名 chan 数据类型
比如定义一个传递int类型数据的Channel:
var ch chan int
此时定义的ch是一个Channel类型的变量,默认值为nil,还不能直接使用,需要通过make函数初始化。
创建Channel必须使用make函数,语法格式:
通道名 = make(chan 数据类型, [缓冲区大小])
根据是否设置缓冲区,Channel分为两种:
// 无缓冲Channelvar ch1 chan intch1 = make(chan int)// 简化写法ch1 := make(chan int)
// 有缓冲Channel,容量为10var ch2 chan intch2 = make(chan int, 10)// 简化写法ch2 := make(chan int, 10)
我们先通过一个简单示例感受Channel的基本使用,实现子goroutine向主goroutine传递消息:
package mainimport ( "fmt" "time")func main() { // 定义并初始化一个bool类型的无缓冲Channel var ch chan bool ch = make(chan bool) // 启动一个子goroutine go func() { for i := 0; i < 10; i++ { fmt.Println("goroutine-", i) } // 模拟耗时操作 time.Sleep(time.Second * 3) // 向Channel中发送数据,告知主goroutine子goroutine执行完成 ch <- true }() // 从Channel中接收数据,主goroutine会阻塞等待,直到有数据传入 data := <-ch fmt.Println("ch data:", data)}
从结果可以看到,主goroutine会一直等待子goroutine执行完成并向Channel发送数据后,才会继续执行。
Channel有三个核心操作:发送数据、接收数据、关闭通道,下面我们分别讲解。
向Channel发送数据的语法格式:
通道名 <- 数据
其中<-是Channel的发送运算符,数据会被发送到Channel中。
// 向ch1发送int类型数据42ch1 := make(chan int)ch1 <- 42
注意:发送数据的类型必须和Channel定义的类型一致,否则会编译报错。
从Channel接收数据有两种常见写法:
变量 := <-通道名
<-是Channel的接收运算符,会从Channel中取出数据并赋值给变量。
ch1 := make(chan int)// 先启动goroutine发送数据,避免死锁go func() { ch1 <- 42}()// 接收数据value := <-ch1fmt.Println("接收的数据:", value) // 输出:接收的数据:42
变量, ok := <-通道名
这种写法可以判断Channel是否已经关闭且没有数据:
ch1 := make(chan int)go func() { ch1 <- 42 close(ch1) // 关闭通道}()// 第一次接收value1, ok1 := <-ch1fmt.Println("value1:", value1, "ok1:", ok1) // 第二次接收(通道已关闭且无数据)value2, ok2 := <-ch1fmt.Println("value2:", value2, "ok2:", ok2) 
关闭Channel使用close函数,语法格式:
close(通道名)
关闭通道后有以下特性:
package mainimport ( "fmt" "time")func test7(ch chan int) { for i := 0; i < 10; i++ { ch <- i // 向通道发送数据 } close(ch) // 发送完数据后关闭通道}func main() { ch1 := make(chan int) go test7(ch1) // 启动goroutine发送数据 // 循环读取通道数据 for { time.Sleep(time.Second) data, ok := <-ch1 if !ok { fmt.Println("读取完毕", ok) break } fmt.Println("ch1 data:", data) }}
Go语言提供了for-range语法,可以简化Channel的读取操作,它会自动判断Channel是否关闭,当Channel关闭且数据读取完毕后,循环会自动退出:
package mainimport ( "fmt" "time")func test7(ch chan int) { for i := 0; i < 10; i++ { ch <- i } close(ch)}func main() { ch1 := make(chan int) go test7(ch1) // for-range读取通道数据 for data := range ch1 { time.Sleep(time.Second) fmt.Println("ch1 data:",data) } fmt.Println("读取完毕")}
运行结果和上面的示例一致,代码更加简洁。
这是Channel最核心的知识点之一,我们从特性、阻塞规则、使用场景三个维度详细讲解。
| 特性 | 无缓冲Channel | 有缓冲Channel |
|---|---|---|
| 缓冲区容量 | 0 | 指定数值(大于0) |
| 通信模型 | 同步通信 | 异步通信 |
| 数据存储 | 无存储,直接传递 | 先存储在缓冲区,按需读取 |
无缓冲Channel也叫同步Channel,发送和接收操作必须同时准备好:
简单理解:无缓冲Channel就像两个人面对面传球,传球的人(发送)必须等接球的人(接收)准备好,否则传球的人只能一直举着球等待。
有缓冲Channel也叫异步Channel,阻塞规则和缓冲区状态相关:
简单理解:有缓冲Channel就像快递柜,快递员(发送方)把包裹放进柜子(缓冲区),不用等收件人(接收方)当场取;只有柜子满了,快递员才需要等待;收件人随时可以取,只有柜子空了才需要等待。
package mainimport "fmt"func main() { ch := make(chan int) // 无缓冲Channel // 启动goroutine接收数据 go func() { fmt.Println("接收方准备接收") value := <-ch fmt.Println("接收方收到数据:", value) }() fmt.Println("发送方准备发送") ch <- 100 // 无缓冲,会阻塞直到接收方准备好 fmt.Println("发送方发送完成")}
package mainimport "fmt"func main() { ch := make(chan int, 3) // 有缓冲Channel,容量3 fmt.Println("初始状态:cap=", cap(ch), "len=", len(ch)) // cap=3 len=0 ch <- 1 fmt.Println("发送1后:cap=", cap(ch), "len=", len(ch)) // cap=3 len=1 ch <- 2 ch <- 3 fmt.Println("发送3后:cap=", cap(ch), "len=", len(ch)) // cap=3 len=3 // 缓冲区已满,再发送会阻塞 // ch <- 4 // 取消注释会导致死锁 // 接收数据 value := <-ch fmt.Println("接收数据:", value) // 接收数据:1 fmt.Println("接收后:cap=", cap(ch), "len=", len(ch)) // cap=3 len=2}
死锁是使用Channel时最容易踩的坑,Go程序运行时会直接panic,提示fatal error: all goroutines are asleep - deadlock!。
当所有goroutine都处于阻塞状态,没有任何一个goroutine可以继续执行,程序就会发生死锁。
package mainfunc main() { ch := make(chan int) ch <- 10 // 死锁!只有main一个goroutine,发送后阻塞,无接收方}package mainfunc main() { ch := make(chan int) go func() { ch <- 10 // 子goroutine发送数据后阻塞,无接收方 }() // main goroutine直接退出,子goroutine永久阻塞,导致死锁}package mainfunc main() { ch := make(chan int, 2) ch <- 1 ch <- 2 ch <- 3 // 缓冲区满,无接收方,死锁}默认的Channel是双向的,既可以发送数据也可以接收数据。Go语言还支持定向Channel,即只能发送或只能接收的Channel,主要用于函数参数约束,避免Channel滥用。
chan<- 数据类型,只能发送数据,不能接收数据<-chan 数据类型,只能接收数据,不能发送数据package mainimport ( "fmt" "time")// 只写Channel作为函数参数,限制函数只能发送数据func writeOnly(ch chan<- int) { ch <- 100 // 合法:发送数据 // value := <-ch // 非法:不能从只写Channel接收数据,编译报错}// 只读Channel作为函数参数,限制函数只能接收数据func readOnly(ch <-chan int) int { data := <-ch // 合法:接收数据 fmt.Println("只读函数接收数据:", data) // ch <- 200 // 非法:不能向只读Channel发送数据,编译报错 return data}func main() { ch1 := make(chan int) // 双向Channel go writeOnly(ch1) // 双向Channel自动转换为只写Channel go readOnly(ch1) // 双向Channel自动转换为只读Channel time.Sleep(time.Second * 3)}
select是Go语言中专门用于处理Channel操作的关键字,可以同时监听多个Channel的发送/接收操作,实现多路复用。
select {case 通道操作1: // 操作1就绪时执行的逻辑case 通道操作2: // 操作2就绪时执行的逻辑default: // 所有通道操作都未就绪时执行的逻辑}package mainimport ( "fmt" "time")func main() { ch1 := make(chan int) ch2 := make(chan int) go func() { time.Sleep(time.Second * 2) ch1 <- 100 }() go func() { time.Sleep(time.Second * 1) ch2 <- 200 }() // 监听两个Channel select { case num1 := <-ch1: fmt.Println("从ch1接收:", num1) case num2 := <-ch2: fmt.Println("从ch2接收:", num2) // default: // fmt.Println("default") }}结合time.After实现Channel操作的超时控制,避免永久阻塞:
package mainimport ( "fmt" "time")func main() { ch := make(chan int) // 子goroutine 3秒后发送数据 go func() { time.Sleep(time.Second * 3) ch <- 100 }() // 只等待1秒,超时则执行default select { case value := <-ch: fmt.Println("成功接收:", value) case <-time.After(time.Second * 1): fmt.Println("接收超时!") }}Channel是Go语言并发编程的核心,常见的应用场景有:
最基础的场景,实现goroutine之间的数据传递和同步,比如我们前面的示例。
这是最经典的并发模式,生产者goroutine生成数据并发送到Channel,消费者goroutine从Channel读取数据并处理。
// 编程实例:电商订单处理 (生产者—消费者模型)type Order struct {ID intUserID stringamount float64status stringcreatedAt time.Time}// 产生订单 --- 生产者func orderProduct(orderChan chan Order, number int) {defer close(orderChan)for i := 0; i < number; i++ {order := Order{ID: i,UserID: fmt.Sprintf("user_%d", rand.Intn(100)),amount: rand.ExpFloat64() * 1000,status: "pending",createdAt: time.Now(),}orderChan <- orderfmt.Printf("生成订单:ID=%d,用户ID=%s,金额=%.2fn", order.ID, order.UserID, order.amount)time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)}}// 处理订单 --- 消费者// <-chan 只接收类型为Order的channel// chan<- 只发送类型为Order的channelfunc orderProcessor(orderChan <-chan Order, resultChan chan<- Order) {defer close(resultChan)for order := range orderChan {fmt.Printf("处理订单:ID=%d,用户ID=%s,金额=%.2fn", order.ID, order.UserID, order.amount)time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)order.status = "completed"resultChan <- order}}// 收集处理结果 --- 消费者func orderResultCollector(resultChan <-chan Order, done chan<- struct{}) {for order := range resultChan {fmt.Printf("订单处理完成:ID=%d,用户ID=%s,金额=%.2f,状态=%sn", order.ID, order.UserID, order.amount, order.status)}done <- struct{}{}}func main() {rand.Seed(time.Now().UnixNano())// 创建管道orderChan := make(chan Order, 10)resultChan := make(chan Order, 10)done := make(chan struct{})// 启动订单生成器go orderProduct(orderChan, 20)// 启动多个订单处理器(工人)for i := 1; i <= 3; i++ {go orderProcessor(orderChan, resultChan)}// 启动结果收集器go orderResultCollector(resultChan, done)// 等待所有处理完成<-done}通过Channel控制goroutine的数量,实现任务的批量处理,避免创建过多goroutine导致资源耗尽。
package mainimport ( "fmt" "time")// 工作goroutinefunc worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf("Worker %d 处理任务 %dn", id, j) time.Sleep(time.Second) results <- j * 2 } close(results)}func main() { jobs := make(chan int, 100) results := make(chan int, 100) // 启动4个worker for w := 0; w <= 3; w++ { go worker(w, jobs, results) } // 发送100个任务 for j := 0; j < 100; j++ { jobs <- j } close(jobs) // 收集结果 for value := range results { fmt.Println("result:", value) }}结合select和time.After实现操作的超时控制,比如网络请求、数据库查询等,模板示例:
package mainimport ("fmt""time")var done = make(chan struct{})func event() {fmt.Println("event执行开始")time.Sleep(4 * time.Second) //4秒fmt.Println("event执行结束")close(done)}func main() {go event()select {case <-done:fmt.Println("协程执行完毕")case <-time.After(3 * time.Second):fmt.Println("超时")return}}
Go语言的time包中的定时器(Timer)和打点器(Ticker)都是基于Channel实现的,是Channel的重要应用场景。
Timer表示一个单次的定时事件,时间到了会向Channel发送当前时间。
package mainimport ( "fmt" "time")func main() { // 创建定时器,3秒后触发 timer := time.NewTimer(time.Second * 3) fmt.Println("当前时间:", time.Now()) // 等待定时器触发 t := <-timer.C fmt.Println("定时器触发:", t) // 提前停止定时器(注释上面的<-timer.C才能测试) // timer.Stop() // fmt.Println("定时器已停止")}
Ticker表示一个重复的定时事件,每隔指定时间就会向Channel发送当前时间。
package mainimport ( "fmt" "time")func main() { // 创建打点器,每隔500毫秒触发一次 ticker := time.NewTicker(500 * time.Millisecond) done := make(chan bool) go func() { for { select { case <-done: return case t := <-ticker.C: fmt.Println("定时触发 at", t.Format("15:04:05")) } } }() // 运行2秒后停止 time.Sleep(2 * time.Second) ticker.Stop() done <- true fmt.Println("Ticker停止")}
time.After是Timer的简化版,返回一个Channel,指定时间后会向该Channel发送当前时间。
package mainimport ( "fmt" "time")func main() { // 3秒后向Channel发送时间 afterChan := time.After(time.Second * 3) fmt.Println("等待3秒...") t := <-afterChan fmt.Println("3秒到了:", t) // 延迟执行函数 time.AfterFunc(time.Second*2, func() { fmt.Println("2秒后执行的函数") }) time.Sleep(time.Second * 3)}
Channel作为Go语言并发编程的核心组件,完全贯彻了“通过通信来共享内存”的设计思想,让goroutine之间的数据传递和同步变得更加简洁安全。
好了,感谢大家的支持,这期的Channel内容就先到这里了,如果有讲解不到位的地方,欢迎大家在评论区指正!