前言
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)// 关闭通道⚠️注意:
- 不要重复关闭同一个通道,会panic。
- 不要向已经关闭的通道发送数据,会panic。
- 从关闭的通道读取数据,不会阻塞,返回对应类型零值。
二、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)}七、常见坑点总结
- nil通道:读写nil通道会永久阻塞。
var ch chan int没有make,直接使用会死锁。 - 忘记close:range读取通道,如果通道不关闭,range会永久阻塞。
- 重复关闭/关闭后发送:直接panic。
- 无缓冲通道忘记goroutine:main直接往无缓冲通道写数据,没有协程接收,直接死锁。
八、总结
- channel是goroutine之间通信的载体,遵循通过通信共享内存,不要共享内存通信。
- 无缓冲通道侧重同步;带缓冲通道侧重数据队列。
- channel三大操作:发送、接收、关闭。
select实现多路通道监听,可以完成多路并发、非阻塞读写、超时控制。for‑range遍历通道,通道关闭自动退出循环。- 单向通道约束函数参数读写权限,提升代码安全性。