golang的引用类型的数据类型有三个chan、slice、map,虽然golang的函数的参数都是值传递,传递引用类型参数只会拷贝参数的变量,实际引用的地址仍然是一个,所以会改变函数外部的变量的值,产生变量逃逸到堆的现象。废话优点多了,今天看一眼golang的chan类型,chan类型是设计用来做goroutine通信的,类似unix的管道,如果了跨进程的通信还是用分布式来解决比较好,chan解决的是goroutine之间的通信。先看一下源码src/runtime/chan.go:hchan 32行,我的版本是1.13.5
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 //chan是否已经关闭
elemtype *_type // element type 元素类型
sendx uint // send index 写入新的元素存放位置
recvx uint // receive index 读取元素的位置
recvq waitq // list of recv waiters 等待读的goroutine队列
sendq waitq // list of send waiters 等待写的goroutine队列
// 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 //互斥锁
}
基本上是实现了一个环形队列,如果是缓冲队列环形队列作为缓冲区,队列的长度make(chan string, len)时由len指定,就是可以缓冲多少个元素,qcount标识还有多少个空闲的缓冲位置,buf指针指向队列的地址,sendx是新写入元素写入的队列的下标,recvx是下次读出数据的队列的下标。
chan通过两个队列和一个互斥锁实现不同goroutine的同步。首先如果chan的缓冲区满了,读写goroutine都会阻塞,会分别在recvq和sendq两个队列里挂起等待被唤醒;在有新的数据写入chan时recvq队列等待的读goroutine会被唤起,同样写队列会在chan缓冲有空位的时候唤起;无缓冲的chan的qcount=0,这时recvq和sendq肯定有一个队列是空的一个不是空的,因为没有位置区缓冲,只能在写入时候唤起读去读出来,同样写也只能在有读队列在等待的情况下写入,否则会panic。同一个chan只能允许一个goroutine读写是lock mutex互斥锁实现的。
chan结构体包含定义的元素的数据格式elemtype,只能存储指定类型的值;elemsize是元素的大小,用于找到buf中的位置。
初始化chan,make(chan string, len)len执行缓冲长度,string指定元素类型,buf大小由长度和元素大小共同决定;下面函数基本上就是创建hchan的数据结构和类型内存安全的判断
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// compiler checks this but be safe.
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
// buf points into the same allocation, elemtype is persistent.
// SudoG's are referenced from their owning thread so they can't be collected.
// TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
var c *hchan
switch {
case mem == 0:
// Queue or element size is zero.
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// Elements do not contain pointers.
// Allocate hchan and buf in one call.
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; elemalg=", elem.alg, "; dataqsiz=", size, "\n")
}
return c
}
写chan就是先判断等待队列是否空,不空就从recvq取出一个goroutine把数据写入,唤醒这个goroutine;如果没有等待的读groutine,就写入缓冲区,缓冲区没有位置就把写入数据写入当前goroutine在sendq排队挂起等待被唤起。
读chan同样,先读缓冲区数据,缓冲区数据,判断sendq是否为空,不空则把goroutine唤起写入缓冲,读结束;没有数据可读则加入recvq队列挂起等待唤起。
关闭chan,会清理recvq和sendq的队列;sendq队列的goroutine会panic,recvq队列的goroutine写入的数据设置为空就是nil;所以关闭的chan继续写入会panic,但是还是能读关闭chan的buf数据。
常用使用demo:
单向队列
func main() {
var c = make(chan int, 10)
writeChan(c)
readChan(c)
}
func readChan(flow <-chan int) {
reciter := <-flow
fmt.Println(reciter)
}
func writeChan(flow chan <-int) {
flow<-1
}
多路io监听由select实现
func addNumberToChan(chanName chan int) {
for {
chanName <- 1
time.Sleep(1 * time.Second)
}
}
func main() {
var chan1 = make(chan int, 10)
var chan2 = make(chan int, 10)
go addNumberToChan(chan1)
go addNumberToChan(chan2)
for {
select {
case e := <-chan1:
fmt.Printf("Get element from chan1: %d\n", e)
case e := <-chan2:
fmt.Printf("Get element from chan2: %d\n", e)
default:
fmt.Printf("No element in chan1 and chan2.\n")
time.Sleep(1 * time.Second)
}
}
}
select监听多路io就是多个chan,没有chan有数据会走default代码块,顺序是随机轮询的没有default语句的话select也会继续循环轮询监听
range循环从chan读数据,就是遍历buf数组,当chan没有数据会阻塞到goroutine
func chanRange(chanName chan int) {
for e := range chanName {
fmt.Printf("Get element from chan: %d\n", e)
}
}