matlab结构体内容引⽤⾃⾮结构体数组对象_Golang的引⽤类
型底层实现
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为2copy(s2, s1) // 将s1拷贝给s2s2[0] = 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 2.4 切⽚ slice 的扩容
当使⽤ append(slice,data) 时候,Golang 会检查底层的数组的长度是否已经不够,如果长度不够,Golang 则会新建⼀个数组,把原数组的数据拷贝过去,再将 slice 中的指向数组的指针指向新的数组。
其中新数组的长度⼀般是⽼数组的俩倍,当然,如果⼀直是俩倍增加,那也会极⼤的浪费内存. 所以在⽼数组长度⼤于 1024 时候,将每次按照不⼩于 25% 的涨幅扩容.
slice 增加长度的源码在 src/ 的 growslice 函数中.
golang 字典 map
map 字典是 golang 中⾼级类型之⼀,它提供键值对形式的存储. 它也是引⽤类型,参数传递时其内部的指针被复制,指向的还是同⼀个内存地址. 当对赋值后的左值进⾏修改时,是会影响到原 map 值的.
map 的底层本质上是实现散列表,它解决碰撞的⽅式是拉链法. map 在进⾏扩容时不会⽴即替换原内存,⽽是慢慢的通过 GC ⽅式释放.
3.1 hmap 结构
以下是 map 的底层结构,其源码位于 src/ 中,结构体主要是 hmap .
// A header for a pe hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/ // Make sure this stays
上述代码中 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 数组的每个元素是链表的头节点.
图 1 内存
在哈希表结构中有⼀个加载因⼦(即 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/ 中,其结构体为 hchan,其中主要包括 buf、sendx、recvx、sendq、recvq 等.
ch0 := make(chan int) // ⽆缓冲通道ch1 := make(chan int, 3) // 有缓冲通道
主要结构的作⽤:
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 上. 然后释放锁.
结构体数组不能作为参数传递给函数参考资料
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论