乐趣区

关于后端:通俗易懂剖析Go-Channel理解并发通信的核心机制

本文来自 Go 待业训练营 小韬同学的投稿。

也强烈安利大家多写博客,不仅能倒逼本人学习总结,也能作为简历的加分项,进步求职面试的竞争力。

你想想看:面试官看到你简历中的博客主页有几十篇文章,几千粉丝是什么感觉。要比你空洞洞的写一句“酷爱技术”强太多啦!

注释

咱们在学习与应用 Go 语言的过程中,对 channel 并不生疏,channel是 Go 语言不同凡响的个性之一,也是十分重要的一环,深刻了解Channel,置信可能在应用的时候更加的得心应手。

一、Channel 根本用法

1、channel 类别

channel在类型上,能够分为两种:
双向 channel:既能接管又能发送的 channel
单向 channel:只能发送或只能接管的 channel,即单向channel 能够为分为:
    + 只写 channel
    + 只读 channel

申明并初始化如下如下:

func main() {  
    // 申明并初始化  
    var ch chan string = make(chan string) // 双向 channel  
    var readCh <-chan string = make(<-chan string) // 只读 channel  
    var writeCh chan<- string = make(chan<- string) // 只写 channel  
}  

上述定义中,<-示意单向的 channel。如果箭头指向chan,就示意只写channel,能够往chan 里边写入数据;如果箭头远离 chan,则示意为只读channel,能够从chan 读数据。

在定义 channel 时,能够定义任意类型的 channel,因而也同样能够定义 chan 类型的 channel。例如:

a := make(chan<- chan int)   // 定义类型为 chan int 的写 channel  
b := make(chan<- <-chan int) // 定义类型为 <-chan int 的写 channel  
c := make(<-chan <-chan int) // 定义类型为 <-chan int 的读 channel  
d := make(chan (<-chan int)) // 定义类型为 (<-chan int) 的读 channel  

channel 未初始化时,其 零值为nilnil 是 chan 的零值,是一种非凡的 chan,对值是 nil 的 chan 的发送接管调用者总是会阻塞。

func main() {  
    var ch chan string  
    fmt.Println(ch) // <nil>  
}  

通过 make 咱们能够初始化一个 channel,并且能够设置其容量的大小,如下初始化了一个类型为 string,其容量大小为512channel

var ch chan string = make(chan string, 512)  

当初始化定义了 channel 的容量,则这样的 channel 叫做 buffered chan,即 有缓冲 channel。如果没有设置容量,channel 的容量为 0,这样的 channel 叫做 unbuffered chan,即 无缓冲channel

有缓冲 channel 中,如果 channel 中还有数据,则从这个 channel 接收数据时不会被阻塞。如果 channel 的容量还未满,那么向这个 channel 发送数据也不会被阻塞,反之则会被阻塞。

无缓冲 channel 则只有当读写操作都筹备好后,才不会阻塞,这也是 unbuffered chan 在应用过程中十分须要留神的一点,否则可能会呈现常见的 bug。

channel 的常见操作:

1. 发送数据

往 channel 发送一个数据应用ch <-

func main() {var ch chan int = make(chan int, 512)  
    ch <- 2000  
}  

上述的 ch 能够是 chan int 类型,也能够是单向chan <-int

2. 接收数据

从 channel 接管一条数据能够应用<-ch

func main() {var ch chan int = make(chan int, 512)  
    ch <- 2000 // 发送数据  
  
    data := <-ch // 接收数据  
    fmt.Println(data) // 2000  
}  

ch 类型是 chan T,也能够是单向<-chan T

在接收数据时,能够返回两个返回值。第一个返回值返回 channel 中的元素,第二个返回值为 bool 类型 ,示意是否胜利地从channel 中读取到一个值。

如果第二个参数是 false,则 示意 channel 曾经被 close 而且 channel 中没有缓存的数据,这个时候第一个值返回的是零值。

func main() {var ch chan int = make(chan int, 512)  
    ch <- 2000 // 发送数据  
  
    data1, ok1 := <-ch // 接收数据  
    fmt.Printf("data1 = %d, ok1 = %t\n", data1, ok1) // data1 = 2000, ok1 = true  
    close(ch)  // 敞开 channel  
    data2, ok2 := <-ch  // 接收数据  
    fmt.Printf("data2 = %d, ok2 = %t", data2, ok2) // data2 = 0, ok2 = false  
}  

所以,如果从 channel 读取到一个零值,可能是发送操作真正发送的零值,也可能是 closed 敞开 channel 并且 channel 没有缓存元素产生的零值,这是须要留神判断的一个点。

3. 其余操作

Go 内建的函数 closecaplen 都能够对 chan 类型进行操作。
close:敞开 channel。
cap:返回 channel 的容量。
len:返回 channel 缓存中还未被取走的元素数量。

func main() {var ch chan int = make(chan int, 512)  
    ch <- 100  
    ch <- 200  
    fmt.Println("ch len:", len(ch)) // ch len: 2  
    fmt.Println("ch cap:", cap(ch)) // ch cap: 512  
}  

发送操作 接管操作 能够作为 select 语句中的case clause,例如:

func main() {var ch = make(chan int, 512)  
    for i := 0; i < 10; i++ {  
       select {  
       case ch <- i:  
       case v := <-ch:  
          fmt.Println(v)  
       }  
    }  
}  

for-range语句同样能够在 chan 中应用,例如:

func main() {var ch = make(chan int, 512)  
    ch <- 100  
    ch <- 200  
    ch <- 300  
    for v := range ch {fmt.Println(v)  
    }  
}  
  
// 执行后果  
100  
200  
300  

2、select 介绍

在 Go 语言中,select语句用于监控一组 case 语句,依据特定的条件执行绝对应的 case 语句或 default 语句,与 switch 相似,但不同之处在于 select 语句中所有 case 中的表达式都必须是 channel 的发送或接管操作。select应用示例代码如下:

select {  
case <-ch1:  
    fmt.Println("ch1")  
case ch2 <- 1:  
    fmt.Println("ch2")  
}  

上述代码中,select关键字让以后 goroutine 同时期待 ch1 的可读和ch2 的可写,在满足任意一个 case 分支之前,select 会始终阻塞上来,直到其中的一个 channel 转为就绪状态时执行对应 case 分支的代码。如果多个 channel 同时就绪的话则随机抉择一个 case 执行。

当应用空 select 时,空的 select 语句会间接阻塞以后的 goroutine,使得该goroutine 进入无奈被唤醒的永恒休眠状态。空 select,即select 内不蕴含任何case

select{}  

另外当 select 语句内只有一个 case 分支时,如果该 case 分支不满足,那么以后 select 就变成了一个阻塞的 channel 读 / 写操作。

select {  
case <-ch1:  
    fmt.Println("ch1")  
}  

上述 select 中,当 ch1 可读时,会执行打印操作,反之则阻塞以后goroutine

select 语句内蕴含 default 分支时,如果 select 内的所有 case 都不满足,则会执行 default 分支的逻辑,用于当其余 case 都不满足时执行一些默认操作。

select {  
case <-ch1:  
    fmt.Println("ch1")  
case ch2 <- 1:  
    fmt.Println("ch2")  
default:  
    fmt.Println("default")  
}  

上述代码中,当 ch1 可读或 ch2 可写时,会执行相应的打印操作,否则就执行 default 语句中的代码,相当于一个非阻塞的 channel 读取操作。

select的应用能够总结为:

select不存在任何的 case 且没有 default 分支:永恒阻塞以后 goroutine;
select只存在一个 case 且没有 default 分支:阻塞的发送 / 接管;
select存在多个 case:随机抉择一个满足条件的case 执行;
select存在 default,其余case 都不满足时:执行 default 语句中的代码;

二、Channel 实现原理

从代码的角度分析 channel 的实现,可能让咱们更好的去应用channel

咱们能够从 chan 类型的数据结构、初始化以及三个操作发送、接管和敞开这几个方面来理解channel

1、chan 数据结构

chan 类型的数据结构定义位于 runtime.hchan,其构造体定义如下:

type hchan struct {  
    qcount   uint           // total data in the queue  
    dataqsiz uint           // size of the circular queue  
    buf      unsafe.Pointer // points to an array of dataqsiz elements  
    elemsize uint16  
    closed   uint32  
    elemtype *_type // element type  
    sendx    uint   // send index  
    recvx    uint   // receive index  
    recvq    waitq  // list of recv waiters  
    sendq    waitq  // list of send waiters  
  
    // lock protects all fields in hchan, as well as several  
    // fields in sudogs blocked on this channel.  
    //  
    // Do not change another G's status while holding this lock  
    // (in particular, do not ready a G), as this can deadlock  
    // with stack shrinking.  
    lock mutex  
}  

解释一下上述各个字段的意义:

qcount:示意 chan 中曾经接管到的数据且还未被取走的元素个数。内建函数 len 能够返回这个字段的值。
datasiz:循环队列的大小。chan在实现上应用一个循环队列来寄存元素的个数,循环队列实用于生产者 - 消费者的场景。
buf:寄存元素的循环队列 bufferbuf 字段是一个指向队列缓冲区的指针,即指向一个dataqsiz 元素的数组。buf 字段是应用 unsafe.Pointer 类型来示意队列缓冲区的起始地址。unsafe.Pointer是一种非凡的指针类型,它能够用于指向任何类型的数据。因为队列缓冲区的类型是动态分配的,所以不能间接应用某个具体类型的指针来示意。
elemtypeelemsizeelemtype示意 chan 中元素的数据类型,elemsize示意其大小。当 chan 定义后,它的元素类型是固定的,即一般类型或者指针类型,因而元素大小也是固定的。
sendx:解决发送数据操作的指针在 buf 队列中的地位。当 channel 接管到了新的数据时,该指针就会加上 elemsize,挪动到下一个地位。buf 的总大小是elemsize 的整数倍且 buf 是一个循环列表。
recvx:解决接收数据操作的指针在 buf 队列中的地位。当从 buf 中取出数据,此指针会挪动到下一个地位。
recvq:当接管操作发现 channel 中没有数据可读时,会被则色,此时会被退出到 recvq 队列中。
sendq:当发送操作发现 buf 队列已满时,会被进行阻塞,此时会被退出到 sendq 队列中。

<p align=center><img src=”https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ddc1b911e2ac40b9ad73bff9647e4dc0~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=395&h=524&s=37043&e=png&b=fbf7f6″ alt=”image.png”  /></p>

2、chan 初始化

channel在进行初始化时,Go 编译器会依据是否传入容量的大小,来抉择调用 makechan64,还是makechanmakechan64 在实现上底层还是调用 makechan 来进行初始化,makechan64只是对 size 做了查看。

makechan函数依据 chan 的容量的大小和元素的类型不同,初始化不同的存储空间。省略一些查看代码,makechan函数的次要逻辑如下:

func makechan(t *chantype, size int) *hchan {  
    elem := t.elem  
      
    ...  
  
    mem, overflow := math.MulUintptr(elem.size, uintptr(size))  
      
    ...  
      
    var c *hchan  
    switch {  
    case mem == 0:  
       // 队列或元素大小为零,不用创立 buf  
       c = (*hchan)(mallocgc(hchanSize, nil, true))  
       c.buf = c.raceaddr()  
    case elem.ptrdata == 0:  
       // 元素不蕴含指针,调配一块间断的内存给 hchan 数据结构和 buf  
       // hchan 数据结构前面紧接着就是 buf,在一次调用中调配 hchan 和 buf  
       c = (*hchan)(mallocgc(hchanSize+mem, nil, true))  
       c.buf = add(unsafe.Pointer(c), hchanSize)  
    default:  
       // 元素蕴含指针,独自调配 buf  
       c = new(hchan)  
       c.buf = mallocgc(mem, elem, true)  
    }  
  
    // 记录元素大小、类型、容量  
    c.elemsize = uint16(elem.size)  
    c.elemtype = elem  
    c.dataqsiz = uint(size)  
    lockInit(&c.lock, lockRankHchan)  
      
    ...  
      
    return c  
}  

3、send 发送操作

Go 在编译发送数据给 channel 时,会把发送操作 send 转换成 chansend1 函数,而 chansend1 函数会调用 chansend 函数。

func chansend1(c *hchan, elem unsafe.Pointer) {chansend(c, elem, true, getcallerpc())  
}  

咱们能够来分段剖析 chansend 函数的实现逻辑。

第一局部:

次要是对 chan 进行判断,判断 chan 是否为 nil,若为nil,则判断是否须要将以后goroutine 进行阻塞,阻塞通过 gopark 来对调用者goroutine park(阻塞休眠)。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {  
    // 第一局部  
    if c == nil { // 判断 chan 是否为 nil  
       if !block { // 判断是否须要阻塞以后 goroutine  
          return false  
       }  
       // 调用这 goroutine park,进行阻塞休眠  
       gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)  
       throw("unreachable")  
    }  
      
    ...  
}  

第二局部

第二局部的逻辑判断是当你往一个容量已满的 chan 实例发送数据,且不想以后调用的 goroutine 被阻塞时(chan未被敞开),那么解决的逻辑是间接返回。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {  
    ...  
    // 第二局部  
    if !block && c.closed == 0 && full(c) {return false}  
    ...  
}  

第三局部

第三局部的逻辑判断是首先进行互斥锁加锁,而后判断以后 chan 是否敞开,如果 chan 曾经被 close 了,则开释互斥锁并 panic,即对已敞开的chan 发送数据会panic

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {  
    ...  
    // 第三局部  
    lock(&c.lock) // 开始加锁  
  
    if c.closed != 0 { // 判断 channel 是否敞开  
        unlock(&c.lock)  
        panic(plainError("send on closed channel"))  
    }  
    ...  
}  

第四局部

第四局部的逻辑次要是判断接管队列中是否有正在期待的接管方 receiver。如果存在正在期待的receiver(阐明此时buf 中没有缓存的数据),则将他从接管队列中弹出,间接将须要发送到 channel 的数据交给这个 receiver,而无需放入到buf 中,让发送操作速度更快一些。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {  
    ...  
      
    // 第四局部  
    if sg := c.recvq.dequeue(); sg != nil {  
       // 找到了一个正在期待的接收者。咱们传递咱们想要发送的值  
       // 间接传递给 receiver 接收者,绕过 channel buf 缓存区(如果 receiver 有的话)  
       send(c, sg, ep, func() {unlock(&c.lock) }, 3)  
       return true  
    }  
  
    ...  
}  

第五局部

当期待队列中并没有正在期待的 receiver,则阐明以后buf 还没有满,此时将发送的数据放入到 buf 中。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {  
    ...  
      
    // 第五局部  
    if c.qcount < c.dataqsiz { // 判断 buf 是否满了  
       // channel buf 还有可用的空间. 将发送数据入 buf 循环队列.  
       qp := chanbuf(c, c.sendx)  
       if raceenabled {racenotify(c, c.sendx, nil)  
       }  
       typedmemmove(c.elemtype, qp, ep)  
       c.sendx++  
       if c.sendx == c.dataqsiz {c.sendx = 0}  
       c.qcount++  
       unlock(&c.lock)  
       return true  
    }  
      
    ...  
}  

第六局部

当逻辑走到第六局部,阐明正在解决 buf 已满的状况。如果 buf 已满,则发送操作的 goroutine 就会退出到发送者的期待队列,直到被唤醒。当 goroutine 被唤醒时,数据或者被取走了,或者 chan 曾经被敞开了。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {  
    ...  
    // 第六局部  
      
    // chansend1 函数调用不会进入 if 块里,因为 chansend1 的 block=true  
    if !block {unlock(&c.lock)  
       return false  
    }  
      
    ...  
      
    c.sendq.enqueue(mysg) // 退出发送队列  
      
    ...  
      
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2) // 阻塞  
      
    ...  
}  

4、recv 接管操作

channel 中接收数据时,Go 会将代码转换成 chanrecv1 函数。如果须要返回两个返回值,则会转换成 chanrecv2chanrecv1 函数和 chanrecv2 都会调用 chanrecv 函数。chanrecv1chanrecv2 传入的 block参数的值是 true,两种调用都是阻塞形式,因而在剖析chanrecv 函数的实现时,能够不思考 block=false的状况。

// 从已编译代码中进入 <-c 的入口点  
func chanrecv1(c *hchan, elem unsafe.Pointer) {chanrecv(c, elem, true)  
}  
  
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {_, received = chanrecv(c, elem, true)  
    return  
}  

同样,省略一些查看类的代码,咱们也能够分段剖析 chanrecv 函数的逻辑。

第一局部

第一局部次要判断以后进行接管操作的 chan 实例是否为 nil,若为nil,则从nil chan 中接收数据的调用这 goroutine 会被阻塞。

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {  
    ...  
    // 第一局部  
    if c == nil { // 判断 chan 是否为 nil  
       if !block { // 是否阻塞,默认为 block=true  
          return  
       }  
       // 进行阻塞  
       gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)  
       throw("unreachable")  
    }  
    ...  
}  

第二局部
这一部分只有是思考 block=falsec为空的状况,block=false的状况咱们能够不做思考。

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {  
    ...  
    // 查看未取得锁的失败非阻塞操作。if !block && empty(c) {...}  
    ...  
}  

第三局部

第三局部的逻辑为判断以后 chan 是否被敞开,若以后 chan 曾经被 close 了,并且缓存队列中没有缓冲的元素时,返回truefalse

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {  
  
    ...  
     
    lock(&c.lock) // 加锁,返回时开释锁  
      
    // 第三局部  
    if c.closed != 0 { // 当 chan 已被敞开时  
        if c.qcount == 0 { // 且 buf 区 没有缓存的数据了  
              
            ...  
              
            unlock(&c.lock) // 解锁  
            if ep != nil {typedmemclr(c.elemtype, ep)  
            }  
            return true, false  
        }  
    }   
    ...  
}  

第四局部

第四局部是解决通道未敞开且 buf 缓存队列已满的状况。只有当缓存队列已满时,才可能从发送期待队列获取到 sender。若以后的chanunbufferchan,即 无缓冲区 channel时,则间接将 sender 的发送数据传递给 receiver。否则就从缓存队列的头部读取一个元素值,并将获取的sender 携带的值退出到 buf 循环队列的尾部。

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {  
    ...  
    if c.closed != 0 {// 当 chan 已被敞开时} else { // 第四局部,通道未敞开  
       // 如果 sendq 队列中有期待发送的 sender  
       if sg := c.sendq.dequeue(); sg != nil {  
          // 存在正在期待的 sender,如果缓存区的容量为 0 则间接将发送方的值传递给接管方  
          // 反之,则从缓存队列的头部获取数据,并将获取的 sender 的发送值退出到缓存队列尾部  
          recv(c, sg, ep, func() {unlock(&c.lock) }, 3)  
          return true, true  
       }  
    }  
      
    ...  
}  

第五局部

第五局部的次要逻辑是解决发送队列中没有期待的 senderbuf中有缓存的数据。该段逻辑与外出的互斥锁共用一把锁,因而不存在并发问题。当 buf 缓存区有缓存元素时,则取出该元素传递给receiver,同时挪动接管指针。

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {  
    ...  
      
    // 第五局部  
    if c.qcount > 0 { // 发送队列中没有期待的 sender,且 buf 中有缓存数据  
        // 间接从缓存队列中获取数据  
        qp := chanbuf(c, c.recvx)  
        if raceenabled {racenotify(c, c.recvx, nil)  
        }  
        if ep != nil {typedmemmove(c.elemtype, ep, qp)  
        }  
        typedmemclr(c.elemtype, qp)  
        c.recvx++ // 挪动接管指针  
        if c.recvx == c.dataqsiz {// 指针若已到开端则进行重置(循环队列)  
           c.recvx = 0  
        }  
        c.qcount-- // 获取数据后,buf 缓存区元素个数减一  
        unlock(&c.lock) // 解锁  
        return true, true  
    }  
  
    if !block { // block=true  
        unlock(&c.lock)  
        return false, false  
    }  
    ...  
}  

第六局部

第六局部的逻辑次要是解决 buf 缓存区中没有缓存数据的状况。当 buf 缓存区没有缓存数据时,那么以后的 receiver 就会被阻塞,直到它从 sender 中接管了数据,或者是 chanclose,才会返回。

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {  
    ...  
    c.recvq.enqueue(mysg) // 将以后接管操作入接管队列  
      
    ...  
      
    // 进行阻塞,期待唤醒  
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)  
    ...  
}  

5、close 敞开

close函数次要用于 channel 的敞开,Go 编译器会替换成 closechan 函数的调用。省略一些查看下的代码后,closechan函数的次要逻辑如下:
+ 如果以后 channil,则间接 panic
+ 如果以后chan 已敞开,再次 close 则间接 panic
+ 如果chan 不为 nilchan 也没有 closed,就把期待队列中的 sender(writer) 和 receiver(reader)从队列中全副移除并唤醒。

func closechan(c *hchan) {  
    if c == nil { // 若以后 chan 未 nil,则间接 panic  
       panic(plainError("close of nil channel"))  
    }  
  
    lock(&c.lock) // 加锁  
      
    if c.closed != 0 { // 若以后 chan 曾经敞开,则间接 panic  
       unlock(&c.lock)  
       panic(plainError("close of closed channel"))  
    }  
      
    ...  
  
    c.closed = 1 // 设置以后 channel 的状态为已敞开  
  
    var glist gList  
  
    // 开释接管队列中所有的 reader  
    for {sg := c.recvq.dequeue()  
       if sg == nil {break}  
       if sg.elem != nil {typedmemclr(c.elemtype, sg.elem)  
          sg.elem = nil  
       }  
       if sg.releasetime != 0 {sg.releasetime = cputicks()  
       }  
       gp := sg.g  
       gp.param = unsafe.Pointer(sg)  
       sg.success = false  
       if raceenabled {raceacquireg(gp, c.raceaddr())  
       }  
       glist.push(gp)  
    }  
  
    // 开释发送队列中所有的 writer (它们会 panic)  
    for {sg := c.sendq.dequeue()  
       if sg == nil {break}  
       sg.elem = nil  
       if sg.releasetime != 0 {sg.releasetime = cputicks()  
       }  
       gp := sg.g  
       gp.param = unsafe.Pointer(sg)  
       sg.success = false  
       if raceenabled {raceacquireg(gp, c.raceaddr())  
       }  
       glist.push(gp)  
    }  
    unlock(&c.lock)  
  
    for !glist.empty() {gp := glist.pop()  
       gp.schedlink = 0  
       goready(gp, 3)  
    }  
}  

三、总结

通过学习 channel 的根本应用,理解其操作背地的实现原理,能够帮忙咱们更好的应用 channel,防止一些操作不当而导致的panic 或者说是 bug,让咱们在应用channel 时可能更加的得心应手。

channel的值和状态有多种状况,而不同的操作 (send、recv、close) 又可能失去不同的后果,这是应用 channel 类型时须要常常留神的点,咱们能够将不同 channel 值下的不同操作进行一个总结,特地留神操作 channel 时会产生 panic 的状况,曾经可能会导致线程阻塞的状况 ,都是有可能导致死锁与goroutine 透露的罪魁祸首。

| channel 执行操作 \channel 状态 | channel 为 nil | channel buf 为空                | channel buf 已满               | channel buf 未满且不为空       | channel 已敞开       |
| ————————— | ———— | —————————— | —————————– | —————————– | ——————- |
receive接管操作           | 阻塞         | 阻塞                           | 读取数据                      | 读取数据                      | 返回 buf 中缓存的数据 |
send发送操作              | 阻塞         | 写入数据                       | 阻塞                          | 写入数据                      | panic           |
close敞开                 | panic    | 敞开 channel,buf 中没有缓存数据 | 敞开 channel,保留已缓存的数据 | 敞开 channel,保留已缓存的数据 | panic

又出问题啦

咱们又出问题啦!大厂 Offer 集锦!遥遥领先!

这些敌人赢麻了!

这是一个专一程序员升职加薪の常识星球

答疑解惑

须要「简历优化」、「就业辅导」、「职业规划」的敌人能够分割我。

加我微信:wangzhongyang1993

关注我的同名公众号:王中阳 Go

退出移动版