Go的Channel在runtime里面是一个hchan的结构体,每次我们make一个新的channel时,runtime从heap内分配一个hchan结构体管理channel。

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:当前channel queue内的element数目,当qcount等于dataqsiz时,表示channel buffer已经满了,send channel会被阻塞;
  • dataqsiz:channel的buffer大小,也就是我们在make时设定的值。dataqsiz在channel创建后不会再变动,因此channel的buffer是不会动态扩容的;
  • buf:buffered channel缓存elements的内存地址。是一个数组实现的环形队列;
  • elemsize:element类型大小;
  • closed:channel是否已经被关闭,防止关闭已经关闭的channel;
  • elemtype:channel element 类型;
  • sendx:buffer中下一个生产的element的index;
  • recvx:buffer中下一个消费的element的index;
  • recvq:阻塞的接收channel,是一个sudog的链表
  • sendq:阻塞的发送channel,是一个sudog的链表

当buffer没有满时,send channel发送elements时直接把elements放在buf内,增加sendx和qcount;当buffer满时,send channel被阻塞,并加入到sendq链表内,等待被唤醒;

当buffer内有elements时,receive channel直接从buffer内去数据,增加recvq,qcount–;当buffer为空时,receive channel被阻塞,并加入到recvq链表内,等待被唤醒。

关闭channel#

当我们调用close(ch)时,runtime会先释放所有阻塞的receive channel或者send channel(因为不可能同时存在阻塞的receive channel 和 send channel)。

释放receive channel时,runtime会将sudog内的element置为nil。而释放send channel时,因为此时channel已经closed,send channel从阻塞状态切换成running,继续send element时,就会panic:

send channel 3
panic: send on closed channel

goroutine 6 [running]:
main.main.func1(0x0, 0x0)
	/home/ubuntu/workspace/go/just_go/main.go:25 +0x65
created by main.main
	/home/ubuntu/workspace/go/just_go/main.go:23 +0x4c
exit status 2

我们可以利用channel的这一特性,实现多个goroutines的优雅退出。在主goroutine中监听信号,收到退出信号后,关闭channel,这时所有的子goroutines都会被唤醒,进行退出处理。

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	c1 := make(chan struct{})
	wg := sync.WaitGroup{}

	for i := 0; i < 4; i++ {
		wg.Add(1)
		go func(i int, c <-chan struct{}) {
			defer wg.Done()
			v := <-c
			fmt.Printf("recevice channel %d, value %T\n", i, v)
		}(i, c1)
	}

	wg.Add(1)
	go func() {
		defer wg.Done()
		time.Sleep(1 * time.Second)
		close(c1)
	}()

	wg.Wait()
}

How Does Golang Channel Works. Understanding Inner workings of the… | by Jerry An | Aug, 2021 | Level Up Coding (gitconnected.com)

GopherCon 2017: Kavya Joshi - Understanding Channels