zoukankan      html  css  js  c++  java
  • golang的引用类型(slice,map,channel)底层实现

    Slice

    切片即动态数组,可以动态扩容改变数组的容量. golang 的 slice 底层结构如下所示,它是一个结构体,里面包含了指向数组的地址,并通过 len、cap 保存数组的元素数、容量:

    type slice struct {
        array unsafe.Pointer // 指向数组的指针
        len   int // 切片中元素的数量
        cap   int // array 数组的总容量
    }

    切片拷贝:

    考虑到切片 slice 的结构,对于切片直接用 = 拷贝,实际上是浅拷贝,只是改变了指针的指向,并没有改变数组中元素的值. 对于深度拷贝的需求,可以借助 copy 内置函数完成. 两种拷贝的方式如下:

    • 深度拷贝: copy(sliceA, sliceB)
    • 浅拷贝: sliceA = sliceB

    切片之间的复制会拷贝数组指针、cap、len 值,但数组指针指向的是同一个地址. 如果想做深度拷贝,即将指针指向的数组内容而不是指针值进行拷贝. 可以使用内置的 copy 函数进行切片拷贝. 如下所示,使用 copy 进行复制,会改变 s2 地址的内存内的数组值.

    var s1 = []int{1, 2}        // 初始化一个切片
    var s2 = make([]int, 2)     // 初始化一个空的切片,cap为2
    copy(s2, s1)                // 将s1拷贝给s2
    s2[0] = 99                  // 改变s2[0]
    fmt.Println(s1[0])          // 打印 1 而不是 99

    切片 slice 函数传递

    在切片进行复制时,会将切片的值(指针、cap、len)复制了一份. 在函数内部可以改变原切片的值.

    但是,当涉及到 append 触发扩容时,原来的指针指向的地址会发生变化,之后再对数组值进行更改,原切片将不受影响.

    //定义一个函数,给切片添加一个元素
    func addOne(s []int) {
        s[0] = 4  // 可以改变原切片值
        s = append(s, 1)  // 扩容后分配了新的地址,原切片将不再受影响
        s[0] = 8 
    }
    var s1 = []int{2}   // 初始化一个切片
    addOne(s1)          // 调用函数添加一个切片
    fmt.Println(s1)     // 输出一个值 [4]

    切片 slice 的扩容

    当使用 append(slice,data) 时候,Golang 会检查底层的数组的长度是否已经不够,如果长度不够,Golang 则会新建一个数组,把原数组的数据拷贝过去,再将 slice 中的指向数组的指针指向新的数组。

    其中新数组的长度一般是老数组的俩倍,当然,如果一直是俩倍增加,那也会极大的浪费内存. 所以在老数组长度大于 1024 时候,将每次按照不小于 25% 的涨幅扩容.

    slice 增加长度的源码在 src/runtime/slice.go 的 growslice 函数中

    Map

    ​map 字典是 golang 中高级类型之一,它提供键值对形式的存储. 它也是引用类型,参数传递时其内部的指针被复制,指向的还是同一个内存地址. 当对赋值后的左值进行修改时,是会影响到原 map 值的.

    map 的底层本质上是实现散列表,它解决碰撞的方式是拉链法. map 在进行扩容时不会立即替换原内存,而是慢慢的通过 GC 方式释放.

    hmap 结构

    以下是 map 的底层结构,其源码位于 src/runtime/map.go 中,结构体主要是 hmap .

    // A header for a Go map.
    type hmap struct {
      // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
      // Make sure this stays in sync with the compiler's definition.
      count     int // # live cells == size of map.  Must be first (used by len() builtin)
      flags     uint8
      B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
      noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
      hash0     uint32 // hash seed
    ​
      buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
      oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
      nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
    ​
      extra *mapextra // optional fields
    }

    上述代码中 buckets、oldbuckets 是指向存储键值的内存地址, 其中 oldbuckets 用于在扩容时候,指向旧的 bucket 地址,再下次访问时不断的将 oldbuckets 值转移到 buckets 中. oldbuckets 并不直接释放内存,而是通过不引用,交由 gc 释放内存.

    散列表和 bucket ( a bucket for a go map)

    hmap 中核心的结构是 buckets,它是 bucket 数组,其中每个 bucket 是一个链表. 这个结构其实就是散列表的实现,通过拉链法消除 hash 冲突. 使得散列表能够存储更多的元素,同时避免过大的连续内存申请. 如下图 1,是 golang buckets 数组在内存中的形式,buckets 数组的每个元素是链表的头节点.

     在哈希表结构中有一个加载因子(即 loadFactor), 它一般是散列包含的元素数除以位置总数. 加载因子越高,冲突产生的概率越高. 当达到一定阈值时,就该为哈希表进行扩容了,否则查询效率将会很低.

    当 golang map 的加载因子大于阈值时,len(map) / 2 ^ B > 6.5 时 ,就会对 map 对象进行扩容. 扩容不会立刻释放掉原来的 bucket 内存,而是由 oldbucket 指向,并产生新的 buckets 数组并由指针 buckets 指向. 在再次访问原数据时,再依次将老的 bucket 移到新的 buckets 数组中. 同时解除对老的 bucket 的引用,GC 会统一释放掉这些内存.

    哈希函数

    哈希函数是哈希表的特点之一,通过 key 值计算哈希,快速映射到数据的地址. golang 的 map 进行哈希计算后,将结果分为高位值和低位值,其中低位值用于定位 buckets 数组中的具体 bucket,而高位值用于定位这个 bucket 链表中具体的 key .

    channel

    hchan 结构

    chan 源码位于 src/runtime/chan.go 中,其结构体为 hchan,其中主要包括 buf、sendx、recvx、sendq、recvq 等.

    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
    }

    主要结构的作用:

    • buf: 有缓冲通道用于存储缓存数据的空间, 它是一个循环链表.
    • sendx 和 recvx: 用于记录循环链表 buf 中的发送或者接受的 index.
    • sendq 和 recvq: 是俩个双向队列,分别是发送、接受的 goroutine 抽象出来的 sudog 结构体的队列.
    • lock: 互斥锁. 在 send 和 recv 操作时锁住 hchan.

    make 创建通道

    使用 make 可以创建通道,如下示例:

    ch0 := make(chan int)   // 无缓冲通道
    ch1 := make(chan int, 3)   // 有缓冲通道

    创建通道实际上是创建了一个 hchan 结构体,返回指针 ch0 . chan 在 go 语言中是引用类型, 在参数传递过程是复制的是这个指针值.

    send 和 recv

    首先会使用 lock 锁住 hchan. 然后以 sendx 或 recvx 序号,在循环链表 buf 指定的位置上找到数据,将数据 copy 到 goroutine 或者时从 goroutine copy 的 buf 上. 然后释放锁.

  • 相关阅读:
    LeetCode 121. Best Time to Buy and Sell Stock
    LeetCode 221. Maximal Square
    LeetCode 152. Maximum Product Subarray
    LeetCode 53. Maximum Subarray
    LeetCode 91. Decode Ways
    LeetCode 64. Minimum Path Sum
    LeetCode 264. Ugly Number II
    LeetCode 263. Ugly Number
    LeetCode 50. Pow(x, n)
    LeetCode 279. Perfect Squares
  • 原文地址:https://www.cnblogs.com/peteremperor/p/14100240.html
Copyright © 2011-2022 走看看