Channel详解:无缓冲vs有缓冲的选择 Channel详解无缓冲vs有缓冲的选择摘要Go channel深入解析包括无缓冲channel同步机制、有缓冲channel异步通信、channel关闭与range遍历、单向channel方向限制、nil channel的妙用、常见死锁场景排查。上篇聊完goroutine有个问题就来了goroutine之间怎么传数据。我刚学Go的时候直接用全局变量加mutex写完被同事吐槽你这是把Java那套硬搬过来了。Go的并发哲学是通过通信来共享内存channel就是干这个的。我第一次用channel的时候踩了个死锁的坑程序跑起来直接hang住排查了两个小时才搞明白无缓冲channel的发送和接收必须同时就绪。这篇我把channel的用法和原理讲透尤其是无缓冲和有缓冲的选择很多人搞混。一、无缓冲Channel无缓冲channel就是make的时候不传第二个参数。它的特点是发送和接收必须同时就绪发送方会阻塞直到有人接收反之亦然。本质上是同步通信。packagemainimportfmtfuncmain(){// 创建无缓冲channel传递int类型数据// make(chan int) 第二个参数为0或不传就是无缓冲ch:make(chanint)// 启动一个goroutine接收数据gofunc(){// 这里会阻塞直到main goroutine发送数据val:-ch fmt.Printf(收到数据: %d\n,val)}()// 发送数据这里也会阻塞直到goroutine准备好接收// 无缓冲channel的发送和接收是同步的类似面对面交接ch-42fmt.Println(发送完成)}无缓冲channel最典型的用途是做信号通知和同步等待。比如等一个goroutine完成初始化后再继续。packagemainimportfmtfuncmain(){// 用无缓冲channel做完成信号done:make(chanstruct{})gofunc(){// 执行一些初始化工作fmt.Println(初始化中...)// 初始化完成发送信号// struct{}不占内存适合做纯信号close(done)}()// 阻塞等待初始化完成// close后的channel接收操作立即返回零值-done fmt.Println(初始化完成继续执行)}二、有缓冲Channel有缓冲channel就是make的时候传第二个参数指定缓冲区大小。发送方在缓冲区没满之前不阻塞接收方在缓冲区没空之前不阻塞。本质上是异步通信。packagemainimportfmtfuncmain(){// 创建缓冲大小为3的channel// 可以缓存3个int值发送方在前3次发送时不会阻塞ch:make(chanint,3)// 连续发送3个数据不会阻塞// 因为缓冲区大小是3刚好放满ch-1ch-2ch-3// 打印当前缓冲区中的数据量fmt.Printf(缓冲区数据量: %d\n,len(ch))// 输出3fmt.Printf(缓冲区容量: %d\n,cap(ch))// 输出3// 接收数据按先进先出的顺序fmt.Println(-ch)// 输出1fmt.Println(-ch)// 输出2fmt.Println(-ch)// 输出3}有缓冲channel适合做生产者-消费者模型。生产者生产数据放进channel消费者从channel取数据处理两边各跑各的。packagemainimport(fmtsynctime)funcmain(){varwg sync.WaitGroup// 创建缓冲大小为10的channel// 相当于在生产者和消费者之间放了一个队列ch:make(chanint,10)// 生产者goroutinewg.Add(1)gofunc(){deferwg.Done()fori:0;i20;i{// 生产数据发送到channel// 缓冲区满了会阻塞等消费者取走数据ch-i fmt.Printf(生产: %d\n,i)time.Sleep(50*time.Millisecond)}// 生产完毕关闭channel// 关闭后消费者还能继续读取剩余数据close(ch)}()// 消费者goroutinewg.Add(1)gofunc(){deferwg.Done()// range遍历channel自动在channel关闭后退出forval:rangech{fmt.Printf( 消费: %d\n,val)time.Sleep(100*time.Millisecond)}fmt.Println(消费完毕)}()wg.Wait()}三、Channel的关闭和遍历channel可以关闭关闭后不能再发送数据但可以继续接收。遍历channel用rangechannel关闭且缓冲区清空后自动退出循环。packagemainimportfmtfuncmain(){ch:make(chanstring,3)// 发送数据ch-helloch-worldch-go// 关闭channel// 关闭后不能再发送否则会panic// 但可以继续接收缓冲区里的数据close(ch)// 方式一用range遍历自动退出formsg:rangech{fmt.Println(msg)}// 方式二用comma ok模式判断是否关闭ch2:make(chanint,2)ch2-1ch2-2close(ch2)// ok为false表示channel已关闭且缓冲区已空for{val,ok:-ch2if!ok{fmt.Println(channel已关闭)break}fmt.Printf(收到: %d\n,val)}}有个规则要记牢channel只能由发送方关闭接收方不要关。向已关闭的channel发送数据会panic。四、单向ChannelGo支持单向channel用于限制channel的方向。函数参数用单向channel可以在编译期防止误操作。packagemainimportfmt// 只能发送的channel类型chan- 表示只能写入funcproducer(outchan-int){fori:0;i5;i{out-i*10// out - i * 10 可以发送数据// val : -out 会编译报错不能接收}close(out)// 发送方负责关闭}// 只能接收的channel类型-chan 表示只能读取funcconsumer(in-chanint){// in - 100 会编译报错不能发送forval:rangein{fmt.Printf(消费数据: %d\n,val)}}funcmain(){// 创建双向channelch:make(chanint,5)// 双向channel可以隐式转换为单向// 编译器自动把chan int转成chan- int或-chan intgoproducer(ch)consumer(ch)}独家踩坑向已关闭的Channel发送数据有一次线上服务突然panic日志里全是send on closed channel。排查发现是一个数据处理管道有多个goroutine往同一个channel写数据某个goroutine处理完后调了close。其他goroutine还在往里写直接panic。代码大概是这样的。// 错误代码多个goroutine写同一个channel其中一个close了funcprocessData(inputs[]int){ch:make(chanint,10)for_,input:rangeinputs{gofunc(valint){result:heavyCompute(val)ch-result// 某个goroutine提前关闭了channel// 其他goroutine还在往里发数据panicclose(ch)// 这里是bug}(input)}forr:rangech{fmt.Println(r)}}正确做法是用sync.WaitGroup等所有goroutine完成后再关闭channel或者用一个单独的goroutine负责关闭。packagemainimport(fmtsync)funcheavyCompute(valint)int{returnval*2}funcprocessData(inputs[]int){ch:make(chanint,10)varwg sync.WaitGroup// 启动多个worker goroutinefor_,input:rangeinputs{wg.Add(1)gofunc(valint){deferwg.Done()result:heavyCompute(val)ch-result// 不在这里关闭channel}(input)}// 单独启动一个goroutine等待所有worker完成// 然后关闭channelgofunc(){wg.Wait()close(ch)// 所有worker都完成了安全关闭}()// 主goroutine消费数据// channel关闭且缓冲区清空后自动退出循环forr:rangech{fmt.Println(r)}}funcmain(){processData([]int{1,2,3,4,5})}对比分析跟Java比Java没有channel这个概念线程间通信靠共享变量加锁或者用BlockingQueue。Go的channel把通信和同步合二为一代码更简洁。Java的BlockingQueue跟有缓冲channel很像但Java没有无缓冲channel的等价物要实现同步交接得用SynchronousQueue。跟Python比Python的queue.Queue是有缓冲队列跟有缓冲channel类似。Python没有无缓冲channel要实现goroutine间的同步交接得用Event或Condition。Python的queue.Queue是线程安全的但没有channel的方向限制。跟Rust比Rust的mpsc channel跟Go的channel类似但Rust区分了单生产者oneshot和多生产者mpsc类型系统更严格。Go的channel默认就是多生产者多消费者灵活但安全性差一些。总结这篇讲了channel的核心用法。无缓冲channel做同步通信发送和接收必须同时就绪。有缓冲channel做异步通信缓冲区没满就不阻塞。channel由发送方关闭用range遍历自动退出。单向channel限制方向防误操作。踩坑那节讲的向已关闭channel发送数据是最常见的channel panic记住用WaitGroup统一管理关闭时机。下一篇讲select语句。当你在多个channel之间等待数据时select是唯一的选择。它能同时监听多个channel哪个先来就处理哪个还能做超时控制。