Array、Slice、Map原理浅析

Array

数组(值类型),是用来存储集合数据的,这种场景很是多,咱们编码的过程当中,都少不了要读取或者存储数据。固然除了数组以外,咱们还有切片、Map映射等数据结构能够帮咱们存储数据,可是数组是它们的基础。javascript

声明和初始化

数组初始化的几种方式java

a := [10]int{ 1, 2, 3, 4 } // 未提供初始化值的元素为默认值 0
b := [...]int{ 1, 2 } // 由初始化列表决定数组⻓度,不能省略 "...",不然就成 slice 了。
c := [10]int{ 2:1, 5:100 } // 按序号初始化元素
复制代码

数组⻓度下标 n 必须是编译期正整数常量 (或常量表达式)。 ⻓度是类型的组成部分,也就是说 "[10]int" 和 "[20]int" 是彻底不一样的两种数组类型。数组

var a [20]int
var b [10]int
// 这里会报错,不一样类型,没法比较
fmt.Println(a == b)
复制代码

数组是值类型,也就是说会拷⻉整个数组内存进⾏值传递。可⽤ slice 或指针代替。数据结构

func test(x *[4]int) {
  for i := 0; i < len(x); i++ {
    println(x[i])
  }
  x[3] = 300
}

// 取地址传入
a := [10]int{ 1, 2, 3, 4 }
test(&a)

// 也能够⽤ new() 建立数组,返回数组指针。
var a = new([10]int) // 返回指针。
test(a)
复制代码

Slice

d

一个slice是一个数组某个部分的引用。在内存中,它是一个包含3个域的结构体:指向slice中第一个元素的指针,slice的长度,以及slice的容量。长度是下标操做的上界,如x[i]中i必须小于长度。容量是分割操做的上界,如x[i:j]中j不能大于容量。app

src/pkg/runtime/runtime.h函数

struct Slice {
  byte* array  // actual data
  uint32 len  // number of elements
  uint32 cap  // allocated number of elements
};
复制代码

对 slice 的修改就是对底层数组的修改。ui

func main() {
	x := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s := x[:6]
	s = append(s, 10)
	s[0] = 100
	fmt.Println(x)
	fmt.Println(s)
}
复制代码

输出编码

[100 1 2 3 4 5 10 7 8 9]
[100 1 2 3 4 5 10]
复制代码

可是当slice的len超出了原底层数组的cap的时候,此时就会新开辟一块内存区域用来存储新建的底层数组。spa

func main() {
	x := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s := x[:]
	s = append(s, 10)
	s[0] = 100
	fmt.Println(x)
	fmt.Println(s)
}
复制代码

输出设计

[0 1 2 3 4 5 6 7 8 9]
[100 1 2 3 4 5 6 7 8 9 10]
复制代码

d

函数 copy ⽤于在 slice 间复制数据,能够是指向同⼀底层数组的两个 slice。复制元素数量受限于src 和 dst 的 len 值 (二者的最⼩值)。在同⼀底层数组的不一样 slice 间拷⻉时,元素位置能够重叠。

func main() {
  s1 := []int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
  s2 := make([]int, 3, 20)
  var n int
  n = copy(s2, s1) // n = 3。不一样数组上拷⻉。s2.len == 3,只能拷 3 个元素。
  fmt.Println(n, s2, len(s2), cap(s2)) // [0 1 2], len:3, cap:20
  s3 := s1[4:6] // s3 == [4 5]。s3 和 s1 指向同⼀个底层数组。
  n = copy(s3, s1[1:5]) // n = 2。同⼀数组上拷⻉,且存在重叠区域。
  fmt.Println(n, s1, s3) // [0 1 2 3 1 2 6 7 8 9] [1 2]
}
复制代码

输出

3 [0 1 2] 3 20
2 [0 1 2 3 1 2 6 7 8 9] [1 2]
复制代码

数组的slice并不会实际复制一份数据,它只是建立一个新的数据结构,包含了另外的一个指针,一个长度和一个容量数据。 如同分割一个字符串,分割数组也不涉及复制操做:它只是新建了一个结构来放置一个不一样的指针,长度和容量。

因为slice是不一样于指针的多字长结构,分割操做并不须要分配内存,甚至没有一般被保存在堆中的slice头部。这种表示方法使slice操做和在C中传递指针、长度对同样廉价。

slice的扩容规则

在对slice进行append等操做时,可能会形成slice的自动扩容。其扩容时的大小增加规则是:

  • 若是新的大小是当前大小2倍以上,则大小增加为新大小
  • 不然循环如下操做:若是当前大小小于1024,按每次2倍增加,不然每次按当前大小1/4增加。直到增加的大小超过或等于新大小。

make和new

Go有两个数据结构建立函数:new和make。基本的区别是new(T)返回一个*T,返回的这个指针能够被隐式地消除引用。而make(T, args)返回一个普通的T。一般状况下,T内部有一些隐式的指针。一句话,new返回一个指向已清零内存的指针,而make返回一个复杂的结构。

总结

  • 多个slice指向相同的底层数组时,修改其中一个slice,可能会影响其余slice的值;
  • slice做为参数传递时,比数组更为高效,由于slice的结构比较小;
  • slice在扩张时,可能会发生底层数组的变动及内存拷贝;
  • 由于slice引用了数组,这可能致使数组空间不会被gc,当数组空间很大,而slice引用内容不多时尤其严重;

Map

Go中的map在底层是用哈希表实现的。Golang采用了HashTable的实现,解决冲突采用的是链地址法。也就是说,使用数组+链表来实现map。

Map存储的是无序的键值对集合。

不是全部的key都能做为map的key类型,只有可以比较的类型才能做为key类型。因此例如切片,函数,map类型是不能做为map的key类型的。

map 查找操做⽐线性搜索快不少,但⽐起⽤序号访问 array、slice,⼤约慢 100x 左右。

经过 map[key] 返回的只是⼀个 "临时值拷⻉",修改其⾃⾝状态没有任何意义,只能从新 value 赋值或改⽤指针修改所引⽤的内存。

每一个bucket中存放最多8个key/value对, 若是多于8个,那么会申请一个新的bucket,并将它与以前的bucket链起来。

注意一个细节是Bucket中key/value的放置顺序,是将keys放在一块儿,values放在一块儿,为何不将key和对应的value放在一块儿呢?若是那么作,存储结构将变成key1/value1/key2/value2… 设想若是是这样的一个map[int64]int8,考虑到字节对齐,会浪费不少存储空间。不得不说经过上述的一个小细节,能够看出Go在设计上的深思熟虑。

数据结构及内存管理

hashmap的定义位于 src/runtime/hashmap.go 中,首先咱们看下hashmap和bucket的定义:

type hmap struct {
  count     int    // 元素的个数
  flags     uint8  // 状态标志
  B         uint8  // 能够最多容纳 6.5 * 2 ^ B 个元素,6.5为装载因子
  noverflow uint16 // 溢出的个数
  hash0     uint32 // 哈希种子

  buckets    unsafe.Pointer // 桶的地址
  oldbuckets unsafe.Pointer // 旧桶的地址,用于扩容
  nevacuate  uintptr        // 搬迁进度,小于nevacuate的已经搬迁
  overflow *[2]*[]*bmap 
}
复制代码

其中,overflow是一个指针,指向一个元素个数为2的数组,数组的类型是一个指针,指向一个slice,slice的元素是桶(bmap)的地址,这些桶都是溢出桶;为何有两个?由于Go map在hash冲突过多时,会发生扩容操做,为了避免全量搬迁数据,使用了增量搬迁,[0]表示当前使用的溢出桶集合,[1]是在发生扩容时,保存了旧的溢出桶集合;overflow存在的意义在于防止溢出桶被gc。

扩容

扩容会创建一个大小是原来2倍的新的表,将旧的bucket搬到新的表中以后,并不会将旧的bucket从oldbucket中删除,而是加上一个已删除的标记。

正是因为这个工做是逐渐完成的,这样就会致使一部分数据在old table中,一部分在new table中, 因此对于hash table的insert, remove, lookup操做的处理逻辑产生影响。只有当全部的bucket都从旧表移到新表以后,才会将oldbucket释放掉。

Golang map 的底层实现

相关文章
相关标签/搜索