Go语言通道(channel):Goroutine通信、同步与select多路并发控制
2026/9/5 9:37:37 网站建设 项目流程

前言

Go语言最核心的并发理念:不要通过共享内存来通信,而要通过通信来实现共享内存
Goroutine是轻量级协程,多个Goroutine之间内存默认互相隔离,如果直接共享变量,就要加锁处理竞争,容易写出bug。而channel(通道)就是Goroutine之间专门用来传递数据、做同步协调的管道,配合select关键字,可以实现多路IO监听,完成复杂并发控制。

一、Channel基础概念

channel 是Go内置类型,类似一个队列,遵循先进先出。专门用于Goroutine之间传递数据,实现协程之间的数据交互与同步。

1. 通道声明与创建

// 声明通道,nil通道,不能直接使用varchchanint// make创建通道ch=make(chanint)// 无缓冲通道chBuf:=make(chanint,3)// 带缓冲通道,缓冲区大小3

语法:make(chan 数据类型, [缓冲大小])

  • 无缓冲通道:发送和接收必须同时就绪,会发生阻塞,用于协程同步。
  • 带缓冲通道:缓冲区未满时发送不会阻塞;缓冲区有数据接收不会阻塞。

2. 通道基本操作

通道一共只有3种操作:发送、接收、关闭

ch<-10// 向通道发送数据10x:=<-ch// 从通道接收数据,赋值给xclose(ch)// 关闭通道

⚠️注意:

  1. 不要重复关闭同一个通道,会panic。
  2. 不要向已经关闭的通道发送数据,会panic。
  3. 从关闭的通道读取数据,不会阻塞,返回对应类型零值。

二、Goroutine之间使用Channel通信

Goroutine之间不共享变量,把数据丢进channel,另一个协程读取,完成数据传递。

packagemainimport"fmt"funcworker(chchanstring){// 子协程:往通道发送数据ch<-"子goroutine完成任务"}funcmain(){ch:=make(chanstring)goworker(ch)// main协程接收子协程的数据msg:=<-ch fmt.Println(msg)fmt.Println("主程序结束")}

输出:

子goroutine完成任务 主程序结束

这里无缓冲通道天然实现同步:main会阻塞等待子协程写入数据,不需要time.Sleep来等待goroutine执行完毕。

三、Channel实现同步

示例1:利用通道等待goroutine执行完毕(替代sync.WaitGroup简易版本)

packagemainimport"fmt"functask(donechanbool){fmt.Println("子协程正在执行任务")done<-true// 任务完成,发送信号}funcmain(){done:=make(chanbool)gotask(done)<-done// main阻塞,等待子协程信号fmt.Println("收到信号,子协程执行完成,main退出")}

通道不仅仅传业务数据,还可以传递信号,做协程同步。

无缓冲 vs 带缓冲通道区别

//无缓冲:发送之后立刻阻塞,必须有人接收ch1:=make(chanint)//缓冲2:可以先写入2个数据,不会阻塞ch2:=make(chanint,2)ch2<-1ch2<-2

四、select:多路通道监听,多路并发控制

select是Go专门为channel设计的语法,作用:同时监听多个channel的读写操作,哪个case通道就绪,就执行哪个case

  • 如果多个case同时就绪,随机选一个执行
  • 如果都没就绪:阻塞,直到有case就绪
  • 搭配default:不会阻塞,没有就绪case直接执行default

1. select基础示例

packagemainimport("fmt""time")funcmain(){ch1:=make(chanstring)ch2:=make(chanstring)gofunc(){time.Sleep(1*time.Second)ch1<-"来自通道1的数据"}()gofunc(){time.Sleep(2*time.Second)ch2<-"来自通道2的数据"}()// select多路监听fori:=0;i<2;i++{select{casev1:=<-ch1:fmt.Println(v1)casev2:=<-ch2:fmt.Println(v2)}}fmt.Println("全部接收完成")}

2. select + default 非阻塞通道操作

没有数据可读,不会卡住,直接走default分支

packagemainimport"fmt"funcmain(){ch:=make(chanint)select{caseval:=<-ch:fmt.Println("读到数据",val)default:fmt.Println("通道暂时没有数据,非阻塞跳过")}}

3. select实现超时控制(开发高频场景)

packagemainimport("fmt""time")funcmain(){ch:=make(chanstring)gofunc(){time.Sleep(3*time.Second)//模拟耗时操作ch<-"任务结果"}()select{caseres:=<-ch:fmt.Println(res)case<-time.After(2*time.Second):// 2秒超时fmt.Println("执行超时!")}}

time.After()返回一个channel,时间到自动发送数据,非常适合做并发超时。

五、通道range遍历

当通道持续发送数据,可以用for range读取通道,通道关闭会自动退出循环

packagemainimport"fmt"funcmain(){ch:=make(chanint,3)ch<-1ch<-2ch<-3close(ch)// range读取通道,通道关闭循环自动结束forv:=rangech{fmt.Println(v)}fmt.Println("读取结束")}

六、单向通道(约束读写权限)

可以定义只读通道、只写通道,在函数参数做约束,代码健壮性更好。

packagemainimport"fmt"// ch 只写通道:只能发送数据funcsend(chchan<-int){ch<-100}// ch 只读通道:只能读取数据funcrecv(ch<-chanint){fmt.Println(<-ch)}funcmain(){ch:=make(chanint)send(ch)recv(ch)}

七、常见坑点总结

  1. nil通道:读写nil通道会永久阻塞。var ch chan int没有make,直接使用会死锁。
  2. 忘记close:range读取通道,如果通道不关闭,range会永久阻塞。
  3. 重复关闭/关闭后发送:直接panic。
  4. 无缓冲通道忘记goroutine:main直接往无缓冲通道写数据,没有协程接收,直接死锁。

八、总结

  1. channel是goroutine之间通信的载体,遵循通过通信共享内存,不要共享内存通信
  2. 无缓冲通道侧重同步;带缓冲通道侧重数据队列。
  3. channel三大操作:发送、接收、关闭。
  4. select实现多路通道监听,可以完成多路并发、非阻塞读写、超时控制。
  5. for‑range遍历通道,通道关闭自动退出循环。
  6. 单向通道约束函数参数读写权限,提升代码安全性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询