Loading...

深入理解Channel底层原理

channel 是什么

Go语言 中的 channel 是一种用于 goroutine 之间通信的机制,它可以帮助我们实现并发编程中的数据传递和同步操作。

Go 语言设计者对并发编程的建议是不要通过共享内存来通信,而是通过通讯来共享内存,这是 Go 语言的设计哲学,而通道就是实现这种设计哲学的工具。通过共享内存来通讯和通过通讯来共享内存是并发编程中的两种编程风格。 当通过共享内存来通讯的时候,我们需要一些传统的并发同步技术(比如互斥锁)来避免数据竞争。而 Go 语言提供的 channel 可以让并发同步通过通讯来共享内存。

channel 的底层数据结构

源码路径:$GOPATH/src/runtime/chan.go

type hchan struct {
    //当前队列中元素的个数。当我们向channel发送数据时,qcount会增加1;当我们从channel接收数据时,qcount会减少1
    qcount   uint

    //如果我们在创建channel时指定了缓冲区的大小,那么dataqsiz就等于指定的大小;否则,dataqsiz为0,表示该channel没有缓冲区。
    dataqsiz uint

    //buf字段是一个unsafe.Pointer类型的指针,指向缓冲区的起始地址。如果该channel没有缓冲区,则buf为nil。
    buf      unsafe.Pointer 

   //表示缓冲区中每个元素的大小。当我们创建channel时,Golang会根据元素的类型计算出elemsize的值。
    elemsize uint16

    // channel 是否已经关闭,当我们通过close函数关闭一个channel时,Golang会将closed字段设置为true。
    closed   uint32        

    //表示下一次接收元素的位置.当我们从channel接收数据时,Golang会从缓冲区中recvx索引的位置读取数据,并将recvx加1
    recvx    uint           

     //表示下一次发送元素的位置。在channel的发送操作中,如果缓冲区未满,则会将数据写入到sendx指向的位置,并将sendx加1。如果缓冲区已满,则发送操作会被阻塞,直到有足够的空间可用。
    sendx    uint          

     // 等待接收数据的 goroutine 队列,用于存储等待从channel中读取数据的goroutine。当channel中没有数据可读时,接收者goroutine会进入recvq等待队列中等待数据的到来。当发送者goroutine写入数据后,会将recvq等待队列中的接收者goroutine唤醒,并进行读取操作。在进行读取操作时,会先检查recvq等待队列是否为空,如果不为空,则会将队列中的第一个goroutine唤醒进行读取操作。同时,由于recvq等待队列是一个FIFO队列,因此等待时间最长的goroutine会排在队列的最前面,最先被唤醒进行读取操作。
    recvq    waitq         

    // 等待发送数据的 goroutine 队列。sendq 字段是一个指向 waitq 结构体的指针,waitq 是一个用于等待队列的结构体。waitq 中包含了一个指向等待队列中第一个协程的指针和一个指向等待队列中最后一个协程的指针。当一个协程向一个 channel 中发送数据时,如果该 channel 中没有足够的缓冲区来存储数据,那么发送操作将会被阻塞,直到有另一个协程来接收数据或者 channel 中有足够的缓冲区来存储数据。当一个协程被阻塞在发送操作时,它将会被加入到 sendq 队列中,等待另一个协程来接收数据或者 channel 中有足够的缓冲区来存储数据。
    sendq    waitq          

    //channel的读写锁,确保多个gorutine同时访问时的并发安全,保证读写操作的原子性和互斥性。当一个goroutine想要对channel进行读写操作时,首先需要获取lock锁。如果当前lock锁已经被其他goroutine占用,则该goroutine会被阻塞,直到lock锁被释放。一旦该goroutine获取到lock锁,就可以进行读写操作,并且在操作完成后释放lock锁,以便其他goroutine可以访问channel底层数据结构。
    lock     mutex 
}

示意图:

20250514141316053.webp

由示意图得知 channel 的底层数据结构是由一个双向链表 (环形队列) 和一个锁组成的。当一个 goroutine 向一个 channel 发送数据时,它会先获取锁,然后将数据添加到链表的末尾,并释放锁。当一个 goroutine 从一个 channel 接收数据时,它也会先获取锁,然后从链表的头部取出数据,并释放锁。

在 Golang 中,每个 channel 都有一个缓冲区 (存在缓冲区为 0 的情况),用于存储传递的数据。当一个 goroutine 向一个 channel 发送数据时,数据会被放入缓冲区中,如果缓冲区已满,那么该 goroutine 会被阻塞到 sendq 的阻塞队列中,直到有足够的空间可以存储数据为止。当一个 goroutine 从一个 channel 接收数据时,数据会从缓冲区中取出,如果缓冲区为空,那么该 goroutine 会被阻塞到 recvq 的阻塞队列中,直到有数据可供接收为止。

当一个 goroutine 向一个已满的 channel 发送数据时,它会被阻塞,并加入到该 channel 的发送队列中。当一个 goroutine 从一个空的 channel 接收数据时,它也会被阻塞,并加入到该 channel 的接收队列中。当有数据可供发送或接收时,Golang 的调度器(GMP)会从相应的队列中取出一个 goroutine,并将其唤醒,以便它可以执行发送或接收操作。

channel 缓冲区

channel 缓冲区是一个连续的内存空间,它是一个环形队列,用于存储 channel 中的元素。缓冲区的大小由 dataqsiz 字段指定。

等待队列

等待队列是一个包含多个 sudog 结构体的链表,用于存储正在等待发送或接收数据的 goroutine。当有数据可用时,等待队列中的 goroutine 会被唤醒并继续执行。

type waitq struct {
    first *sudog // 队列头部指针
    last  *sudog // 队列尾部指针
}

sudog 结构体

sudog 是 channel 最核心的数据结构。sudog 代表了一个在等待队列中的 goroutine,它包含了等待的 goroutine 的信息,如等待的 channel、等待的元素值、等待的方向(发送或接收)等。

// sudog 结构体是一个用于等待队列中的 goroutine 的结构体,
// 它包含了等待的 goroutine 的信息,如等待的 channel、等待的元素值、
// 等待的方向(发送或接收)等。
type sudog struct {
    // 等待的 goroutine
    g *g

    // 指向下一个 sudog 结构体
    next *sudog

    // 指向上一个 sudog 结构体
    prev *sudog

    //等待队列的元素
    elem unsafe.Pointer

    // 获取锁的时间
    acquiretime int64

    // 释放锁的时间
    releasetime int64

    //用于实现自旋锁。当一个gorutine需要等待另一个gorutine操作完成,
    //而等待时间很短的情况下就会使用自旋锁。
    //它会先获取当前的ticket值,并将其加1。然后,它会不断地检查结构体中的ticket字段是否等于自己的ticket值,
    //如果相等就说明获取到了锁,否则就继续自旋等待。当锁被释放时,另一个goroutine会将ticket值加1,从而唤醒等待的goroutine。
    //需要注意的是,自旋锁适用于等待时间很短的场景,如果等待时间较长,就会造成CPU资源的浪费
    ticket uint32

    // 等待的 goroutine是否已经被唤醒
    isSelect bool

    //success 表示通道 c 上的通信是否成功。
    //如果 goroutine 是因为在通道 c 上接收到一个值而被唤醒,那么 success 为 true;
    //如果是因为通道 c 被关闭而被唤醒,那么 success 为 false。
    success bool

    //用于实现gorutine的堆栈转移
    //当一个 goroutine 调用另一个 goroutine 时,它会创建一个 sudog 结构体,并将自己的栈信息保存在 sudog 结构体的 parent 字段中。
    //然后,它会将 sudog 结构体加入到等待队列中,并等待被调用的 goroutine 执行完成。
    //当被调用的 goroutine 执行完成时,它会将 sudog 结构体从等待队列中移除,并将 parent 字段中保存的栈信息恢复到调用者的栈空间中。
    //这样,调用者就可以继续执行自己的任务了。
    //需要注意的是,sudog 结构体中的 parent 字段只在 goroutine 调用其他 goroutine 的时候才会被使用,
    //因此在普通的 goroutine 执行过程中,它是没有被使用的。
    parent *sudog // semaRoot binary tree

    //用于连接下一个等待的 sudog 结构体
    //等待队列是一个链表结构,每个 sudog 结构体都有一个 waitlink 字段,用于连接下一个等待的 sudog 结构体。
    //当被等待的 goroutine 执行完成时,它会从等待队列中移除对应的 sudog 结构体,
    //并将 sudog 结构体中的 waitlink 字段设置为 nil,从而将其从等待队列中移除。
    //需要注意的是,waitlink 字段只有在 sudog 结构体被加入到等待队列中时才会被使用。
    //在普通的 goroutine 执行过程中,waitlink 字段是没有被使用的。
    waitlink *sudog // g.waiting list or semaRoot

    //待队列的尾部指针,waittail 字段指向等待队列的尾部 sudog 结构体。
    //当被等待的 goroutine 执行完成时,它会从等待队列中移除对应的 sudog 结构体,并将 sudog 结构体中的 waitlink 字段设置为 nil,
    //从而将其从等待队列中移除。同时,waittail 字段也会被更新为等待队列的新尾部。
    //需要注意的是,waittail 字段只有在 sudog 结构体被加入到等待队列中时才会被使用。
    //在普通的 goroutine 执行过程中,waittail 字段是没有被使用的。
    waittail *sudog // semaRoot

    //在golang中,goroutine是轻量级线程,其调度由golang运行时系统负责。当一个goroutine需要等待某些事件的发生时,
    //它可以通过阻塞等待的方式让出CPU资源,等待事件发生后再被唤醒继续执行。这种阻塞等待的机制是通过wait channel实现的。
    //在sudog结构体中,c字段指向的wait channel是一个用于等待某些事件发生的channel。
    //当一个goroutine需要等待某些事件时,它会创建一个sudog结构体,并将该结构体中的c字段指向wait channel。
    //然后,它会将该sudog结构体加入到wait channel的等待队列中,等待事件发生后再被唤醒继续执行。
    //当一个goroutine需要等待某些事件时,它会将自己加入到wait channel的等待队列中,并阻塞等待事件发生。
    //当事件发生后,wait channel会将等待队列中的goroutine全部唤醒,让它们继续执行。
    //这种机制可以有效地避免busy waiting,提高CPU利用率。
    c *hchan // channel
}

sudog 中所有字段都受 hchan.lock 保护。acquiretime、releasetime、ticket 这三个字段永远不会被同时访问。对 channel 来说,waitlink 只由 g 使用。只有在持有 semaRoot 锁的时候才能访问这三个字段。isSelect 表示 g 是否被选择,g.selectDone 必须进行 硬件同步原语(CAS) 才能在被唤醒的竞争中胜出。success 表示 channel c 上的通信是否成功。如果 goroutine 在 channel c 上传了一个值而被唤醒,则为 true;如果因为 c 关闭而被唤醒,则为 false。

g 结构体

g 结构体是 Golang 中的 goroutine 结构体,这个结构体比较复杂,它包含了 goroutine 的运行状态、栈信息、等待的信号等信息。

type g struct {
    stack       stack   // goroutine 的栈信息
    atomicstatus atomic.Uint32 // goroutine 的运行状态
    sched       gobuf   // goroutine 调度器上下文信息
    atomicstatus uint32 // 原子级别的 goroutine 运行状态
    waitreason  waitReason // goroutine 等待的原因

    sig         uint32  // goroutine 等待的信号
    ...
}

有缓冲的 channel 和无缓冲的 channel 的区别

channel 按照读写分类,可以分为下面几类:

  • 单向只读 channel
var readOnlyChan <-chan int  // channel 的类型为 int
  • 单向只写 channel
var writeOnlyChan chan<- int
  • 双向可读可写 channel
var ch chan int
注意⚠️:
写一个只读的
channel 或者读一个只写的 channel 都会导致编译不通过。
双向
channel 可以隐式装换成单向 channel,但是单向 channel 无法隐式转换成双向 channel, 单向只读 channel 和单向只写 channel 之间也不能互相转换
    var c chan int
    var c2 chan<- int
    var c3 <-chan int
    c2 = c
    c3 = c
    c = c2 //编译不通过
    c = c3 //编译不通过
    c2 = c3 //编译不通过
    fmt.Println(c2, c3)

按照有无缓冲可以分为下面两类:

每个 channel 值有一个容量属性。 一个容量为 0 的 channel 值称为一个非缓冲通道(unbuffered channel),一个容量不为 0 的通道值称为一个缓冲通道(buffered channel

channel 类型的零值也使用预声明的 nil 来表示。 一个非零通道值必须通过内置的 make 函数来创建。 比如 make (chan int, 1) 将创建一个元素类型为 int 且容量为 1 的通道值。 第二个参数指定了这个通道的容量。容量这个参数是可选的,它的默认值为 0。

  1. 带缓冲区的 channel,定义了缓冲区大小,可以存储多个数据;
  2. 不带缓冲区的 channel,只能存一个数据,并且只有当该数据被取出才能存下一个数据.
readOnlyChanWithBuff := make(<-chan int, 2)  // 只读且带缓存区的 channel

readOnlyChan := make(<-chan int)   // 只读且不带缓存区 channel

writeOnlyChanWithBuff := make(chan<- int, 4) // 只写且带缓存区 channel

writeOnlyChan := make(chan<- int) // 只写且不带缓存区 channel

ch := make(chan int, 10)  // 可读可写且带缓存区

无缓冲的 channel

无缓冲的 channel 也叫做同步 channel,它的特点是发送和接收操作是同步的。当一个 goroutine 向一个无缓冲的 channel 发送数据时,它会被阻塞,直到另一个 goroutine 从该 channel 中接收数据为止。同样地,当一个 goroutine 从一个无缓冲的 channel 中接收数据时,它也会被阻塞,直到另一个 goroutine 向该 channel 中发送数据为止。

20250514143341096.webp

需要注意的是,这里所说的阻塞是指我们正确使用 channel 的情况下,对于无缓冲的 channel 来说,只有当发送方和接收方同时准备好才不会导致死锁,这也就意味着我们需要在读取 channel 前异步写 channel

func f1() {
  ch := make(chan int)
  go func() {
    ch <- 1
  }()
  fmt.Println(<-ch)
}

比如上面的代码中我们是可以正常读写一个无缓冲的 channel

如果我们在新的协程读取一个 channel 或者写一个 channel 都不会发生死锁,比如:

//只写不读
func f2() {
  ch := make(chan int)
  go func() {
    fmt.Println("开始写入ch")
    ch <- 1 //只会阻塞在当前协程中
    fmt.Println("写入ch完成") //没有机会执行
  }()
}

//只读不写
func f3() {
  ch := make(chan int)
  go func() {
   fmt.Println("开始读取ch")
   fmt.Println(<-ch)//永久阻塞,随主协程一起退出
   fmt.Println("读取ch完成") //没有机会执行
  }()

  time.Sleep(time.Second * 3)
}

//读写都在不同的协程中,则先声明读channel也不会导致死锁了
func f4() {
  ch := make(chan int)
  go func() {
    fmt.Println(<-ch)
  }()

  go func() {
    ch <- 1
  }()
  time.Sleep(time.Second)
}

//如果我们先读,再异步写channel是会导致死锁的,因为读的动作就已经阻塞住了,后面写的工作没机会执行,死锁
func f5() {
  ch := make(chan int)
  fmt.Println(<-ch)
  go func() {
    ch <- 1
  }()
}

//在同一个协程中读写channel,死锁
func f6() {
  ch := make(chan int)
  ch <- 1 //写入时会一直阻塞,等待读取
  <-ch //读取时,由于上面已经阻死了,永远走不到这里
}

//在同一个协程中写channel但没有读,死锁
func f7() {
  ch := make(chan int)
  ch <- 1
}

//在同一个协程中只读,死锁
func f8() {
  ch := make(chan int)
  <-ch
}

//相互等待,死锁
func f9() {
    ch := make(chan int)
    ch1 := make(chan int)
    go func() {
        for {
            select {
            case <-ch:
                ch1 <- 1
            }
        }
    }()

    for {
        select {
        case <-ch1:
            ch <- 1
        }
}


//在同一个协程中读写无缓冲的channel并不一定会导致死锁,如果读写channel之前存在耗时很长的协程,则会阻塞住当前协程
 func f10() {
    var ch chan int
    go test()
    fmt.Println("永久阻塞")
    <-ch
}

func test() {
    for {
        time.Sleep(time.Second)
    }
}

有缓冲的 channel

有缓冲的 channel 也叫做异步 channel,它的特点是发送和接收操作是非阻塞的。当一个 goroutine 向一个有缓冲的 channel 写数据时,如果该 channel 的缓冲区未满,发送操作会立即成功;否则,发送操作也会被阻塞,直到缓冲区有足够的空间。同理,当一个 goroutine 从一个有缓冲的 channel 中接收数据时,如果该 channel 的缓冲区非空,接收操作会立即成功;否则,接收操作会被阻塞,直到缓冲区有数据为止。

20250514143624503.webp

无缓冲 channel 明显不同的是,有缓冲的 channel 由于存在一定的缓冲空间,在缓冲空间未满的情况下,在同一个 gorutine 中进行读写并不会阻塞而发生死锁。注意这里的前提条件是缓冲区未满的情况下。比如:

//我们创建了一个有缓冲的channel,缓冲区大小为1。
//然后我们向该channel中发送数据,并通过`<-ch`从该channel中接收数据,并打印出来。
//由于该channel是有缓冲的,因此发送和接收操作是非阻塞的,发送操作可以立即成功,并将数据放入缓冲区中,
//接收操作也可以立即成功,从缓冲区中取出数据并打印出来
func f11() {
  ch := make(chan int, 1)
  ch <- 1
  fmt.Println(<-ch)
}

//缓冲区已满,发送方阻塞,没有接收方而发生死锁
func f12() {
   ch := make(chan int, 1)
   ch <- 1
   ch <- 2
}

//只接收没有发送导致死锁
func f12_1() {
   ch := make(chan int, 1) 
   fmt.Println(<-ch)
}

//缓冲区不为空,接收方阻塞而无法开启新的发送方而导致死锁
func f13() {
    ch := make(chan int, 1)
    <-ch
    ch <- 1
}

//两个goroutine中有缓冲的channel相互等待而产生死锁
func f14() {
    ch := make(chan int, 2)
    ch1 := make(chan int, 2)
    go func() {
        for {
            select {
            case <-ch:
                ch1 <- 1
            }
        }
    }()

    for {
        select {
        case <-ch1:
            ch <- 1
        }
    }
}

使用 channel 的注意事项

关闭 channel 后并不为 nil

首先一个未初始化的 channel 肯定是 nil 的,channel 的零值是 nil。这里需要注意的是一个关闭的 channel 是不是 nil 呢?

  var ch chan struct{}
    var ch1 = make(chan struct{})
    fmt.Println("ch == nil:", ch == nil)         //ch == nil: true
    fmt.Println("ch1 == nil:", ch1 == nil)      //ch1 == nil: false
    close(ch1)
    fmt.Println("关闭后ch1 == nil:", ch1 == nil) //关闭后ch1 == nil: false

如上所示,关闭后的 channel 并不为 nil

向 nil 的 channel 发送和读取数据会导致 panic

channel 与map 一样,也是需要先使用 make 初始化后才能使用的,如果我们直接向一个 nil 的 channel 发送或者接收数据都会发生阻塞而导致死锁

func f1() {
    var c chan int
    go func() {
        c <- 1
    }()
    fmt.Println(<-c)
}

输出:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive (nil chan)]:

在前面小节中我们使用 make 初始化后执行了相同的代码,使用 make 初始化后的代码是可以正常执行的,但这里 nilchannel 是会导致 panic 的,也就是 nilchannel 是无法单独在当前协程中发送和接收消息的,即使是在当前协程中接收然后新开协程发送,或者在当前协程中发送,在新的协程中接收,都会导致死锁而产生 panic.

需要注意的是,在当前协程中向 nilchannel 中发送或读取数据,是因为阻塞而导致的死锁,并不是由于空指针导致的异常。如果读取和发送都是新的协程中进行,则新的协程会一直阻塞直到主协程退出,可以用这个示例验证:

func f2() {
    var c chan int
    go func() {
        fmt.Println("开始发送")
        c <- 1
        fmt.Println("阻塞在发送") //永远不会执行
    }()
    go func() {
        fmt.Println("开始接收")
        fmt.Println(<-c)
        fmt.Println("阻塞在接收") //永远不会执行
    }()
    time.Sleep(time.Second)
}

上面的例子中 c 是一个 nilchannel, 在新的协程中发送和接收数据,会一直阻塞在发送和接收的地方,直到随主协程退出。最终输出的结果为:

开始发送
开始接收

由此我们可以得出结论:在当前协程中,向 nilchannel 中发送数据或者从一个 nilchannel 接收数据都会发生阻塞而导致死锁。

nil 的 channel 关闭数据会导致 panic

需要注意的是,nilchannel 是不能执行关闭操作的,向一个 nilchannel 关闭数据会导致 panic.

当我们尝试关闭一个 nil channel 时,程序会抛出一个 panic 异常。因为 nil channel 并不是一个可以被关闭的 channel。可以看下面的示例代码:

var c chan int
close(c) // panic: close of nil channel

向关闭 channel 发送数据会 panic

在前面介绍过,当一个 channel 关闭后,它并不是一个 nil channel。那如果我们向一个已经关闭的 channel 中发送数据看看会发生什么

func f1() {
    ch := make(chan int)
    close(ch)
    go func() {
        fmt.Println("开始接收数据")
        fmt.Println("<-ch:", <-ch)
        fmt.Println("数据接收完成")
    }()

    go func() {
        fmt.Println("开始发送数据")
        ch <- 1
        fmt.Println("发送数据完毕")
    }()
    time.Sleep(time.Second)
}

输出:

开始发送数据 
开始接收数据 
<-ch: 0
panic: send on closed channel

goroutine 7 [running]:
main.f1.func2()

向关闭的 channel 读取数据可以正常读取

其实可以从关闭的 channel 读取数据的,并不会发现死锁或者 panic 的,会读取到这个 channel 中数据类型的默认值 ch := make(chan int),比如这里 channel 的数据类型为 int,其默认值为 0,所以我们读取出的值为 0。看看下面示例就一目了然:

func f2() {
    ch := make(chan int)
    close(ch)
    for {
        select {
        case c := <-ch:
            fmt.Println(c)
            time.Sleep(time.Second)
        }
    }
}

输出:

0
0
0
0
0
0
0
...

而且从一个关闭的 channel 中读取数据是不会阻塞的,即使这个 channel 是一个无缓冲的 channel:

func f3() {
    ch := make(chan int)
    close(ch)
    <-ch          //读取已经关闭的channel ,不会阻死
    println("完成") //这句会输出
}

func f4() {
    ch := make(chan int)
    go func() {
        close(ch)
        println("已关闭ch") //这句会输出
    }()
    <-ch          //这里读取时也不会组塞住
    println("完成") //这句会输出
}

所以在关闭一个 channel 之前,如果我们无法确定这个 channel 是不是 nil,也得先判断这个 channel 是不是 nil 再进行关闭。我们已经知道了向一个关闭的 channel 中发送数时会导致 panic,所以我们在向一个 channel 中发送数据时,需要先判断这个 channel 是否已经关闭了。那么我们应该怎么判断一个 channel 是否已经关闭了呢?首先我们需要了解 channel 关闭后的一些特性:

  • 当我们使用 range 语句遍历一个 channel 时,当 channel 关闭时,range 语句才会结束执行
func f5() {
    ch := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            ch <- i
            time.Sleep(time.Second)
        }
        close(ch)
    }()

    for x := range ch {
        fmt.Println(x)
    }
}

我们通常在使用 channel 之前就需要对 channel 进行判断,所以这个特性一般不用来判断一个 channel 是够已经关闭

  • 读取 channel 时,如果 channel 已经关闭,会返回一个零值和一个标识,我们可以通过这个标识来判断 channel 是否已经关闭,在实际使用中我们通常会结合 select 来读取 channel
func f6() {
    ch := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            ch <- i
        }
        close(ch)
    }()

    for {
        select {
        case data, ok := <-ch:
            if !ok {
                fmt.Println("channel closed")
                return
            }
            fmt.Println("data:", data)
        }
    }
}

输出:

data: 0
data: 1
data: 2
data: 3
data: 4
data: 5
data: 6
data: 7
data: 8
data: 9
channel closed
注意⚠️:关闭已经关闭的 channel 也会引发 panic

不要使用 channel 传输大的数据

channel 中传输数据的大小是有限制的,Go 官方编译器中 channel 最大能传输的尺寸为 64KB。比如下面例子在官方编译器中将会导致编译错误:

func f10() {
    arr := [4096]string{}
    ch := make(chan [4096]string)
    go func() {
        ch <- arr
    }()
    time.Sleep(time.Second)
}

输出:

20250514145944120.webp

主要原因是 channel 在传递过程中元素值是需要经过复制的,在一个值从一个协程传递到另一个协程的过程中,这个值将至少被复制一次,如果传递的这个值在某个 channel 的缓存队列中停留过,则这个值在传递的过程中将被复制两次。

一次复制发生在从发送协程向缓冲队列推入此值的时候,另一个复制发生在接收协程从缓冲队列取出此值的时候。 和赋值以及函数调用传参一样,当一个值被传递时,只有它的直接部分被复制。所以为了避免过大的复制成本,官方编译器对 channel 的元素尺寸进行了限制,我们在平时编程过程中也需要注意,不要使用 channel 传输过大的数据

通道和协程的垃圾回收

需要注意的是:一个 channel 会被它的发送队列的协程和接收数据的所有协程引用着,如果一个 channel 的发送队列或接收队列有一个不为空,那这个通道肯定不会被垃圾回收。

对于协程而言,如果一个协程处于某个 channel 的某个协程队列之中,即使这个 channel 只被这个协程所引用,这个协程也不会被垃圾回收,只有当这个协程退出后才能被垃圾回收。

总结

channel常见的异常
nil非空缓冲满了缓冲没满
接收阻塞接收值阻塞接收值接收值
发送阻塞发送值发送值阻塞发送值
关闭panic成功并读完数据后返回 0成功并返回0成功并读完数据后返回 0成功并读完数据后返回 0
参考: https://learnku.com/articles/89660
https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/channel.html

0

回到顶部