数组(值类型),是用来存储集合数据的,这种场景很是多,咱们编码的过程当中,都少不了要读取或者存储数据。固然除了数组以外,咱们还有切片、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是一个数组某个部分的引用。在内存中,它是一个包含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]
复制代码
函数 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进行append等操做时,可能会形成slice的自动扩容。其扩容时的大小增加规则是:
Go有两个数据结构建立函数:new和make。基本的区别是new(T)返回一个*T,返回的这个指针能够被隐式地消除引用。而make(T, args)返回一个普通的T。一般状况下,T内部有一些隐式的指针。一句话,new返回一个指向已清零内存的指针,而make返回一个复杂的结构。
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释放掉。