我是靠谱客的博主 善良西装,最近开发中收集的这篇文章主要介绍golang 解析struct为map_Golang 的引用类型底层实现,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

ec9f4bdd7d1455c1326aa1e432d6b356.png
golang 有三个常用的高级类型 slice、map、channel, 它们都是引用类型. 掌握引用类型的底层原理,可以在写程序时避免一些坑.

golang 的引用类型

golang 是一个值传递的语言,在函数调用时候传递的参数是拷贝的副本,这就意味着函数内部的变量值改变不影响原变量. 不过,熟悉 go 的人了解,对于 slice、map、channel 这几个类型. 在传递给函数后,函数内部的变量操作依然会影响到原变量. 因为这几个类型是引用类型.

golang 的引用类型包括 slice、map、channel、function、pointer 等. 它们在进行赋值时拷贝的是指针值,但拷贝后指针指向的地址是相同的.

本文将简析 slice、map、channel 这三个引用类型. 从它们的底层实现上,探究在进行参数传递时的变量拷贝情况.

golang 的切片 slice

2.1 slice 底层结构

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

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

2.2 切片 slice 赋值

切片 slice 的赋值操作是改变了 slice 内部结构的值. 所以赋值后改变的是指针 array 指向的地址、len 和 cap 值. 赋值操作的左、右俩个切片的 array 指向的是同一个数组,所以它们的数组中元素的值也会一起发生改变.

2.3 切片的拷贝

考虑到切片 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

2.3 切片 slice 函数传递

golang 函数的参数传递都是值传递,而 map、channel、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]

2.4 切片 slice 的扩容

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

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

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

golang 字典

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

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

3.1 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 释放内存.

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

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

51ff98eb6f14af4092ee11f45ec82da0.png
图 1 buckets

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

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

3.3 哈希函数

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

通道channel

4.1 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.

4.2 make 创建通道

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

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

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

4.3 send 和 recv

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

参考资料

  1. Golang 切片Slice原理详解和使用技巧[https://roperluo.cn/index.php/archives/22/]
  2. Golang:值类型与引用类型[https://www.jianshu.com/p/18d3bde7d835]
  3. Golang map的底层实现[https://www.cnblogs.com/maji233/p/11070853.html]
  4. 图解Golang的channel底层原理[https://juejin.im/post/5cb3445f6fb9a068b748ab75]
  5. Golang 中 slice cap 增长模式小记[https://h3l.github.io/posts/slice-append-grow-analyze/]

最后

以上就是善良西装为你收集整理的golang 解析struct为map_Golang 的引用类型底层实现的全部内容,希望文章能够帮你解决golang 解析struct为map_Golang 的引用类型底层实现所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(69)

评论列表共有 0 条评论

立即
投稿
返回
顶部