七叶笔记 » golang编程 » 详解golang的数据类型和底层实现一

详解golang的数据类型和底层实现一

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) 
    }
}  

相关文章