前言
golang的数组和C/C++没有什么大的区别,而且slice与C++的容器vector也有着一丝相似之处,下面来记录一下我学习完golang的数组和切片的一些收获。
一、数组
1.概念
1.1golang里面的数组和其他语言没什么区别,是一个长度固定的数据类型,里面存储着一组具有相同类型的元素。存储类型可以是内置类型也可以是结构体类型。数组在初始化以后大小就不能改变了。
1.2数组在底层的描述
type Array struct {
Elem *Type // element type
Bound int64 // number of elements; <0 if unknown yet
}
2.声明和初始化方式
2.1 初始化方式
(1)只声明而不设置任何值,默认初始化的值为对应类型的零值
var array [5]int ------>0,0,0,0,0
var array [5]bool -------->false,false,false,false,false
(2)字面量声明并初始化数组
array := [5]int{1,2,3,4,5}
(3)由go自动计算声明数组的长度
array := […]int{1,2,3,4,5}
(4)只指定特定元素的值来声明数组
array := [5]int{1:10, 2:20} ----->0,10,20,0,0
2.2 不同初始化方式的区别
(1)第(2)种声明方式,数组的大小在类型检查时期编译器就能够确定了,而第(3)种则需要编译器对数组的大小进行推到,以遍历的方式计算出数据元素的个数,然后进行初始化。但是这两种方式对于运行期间的程序是没有区别的,它们都是在编译期间确定了大小并完成了初始化。
(2)对于一个由字面量组成的数组即以(3)形式初始化的数组,根据元素数量的不同编译器做了一定的优化:
当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上;
当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出;
3.使用方式
(1)取值:数组名+[index]
(2)赋值操作
相同类型(长度相同,元素类型相同)的数组之间可以相互赋值,数组名代表整个数组。
3.1访问越界处理办法
(1)编译器会在编译期间的静态类型检查阶段判断数组是否越界
i:访问数组的索引是非整数时,报错 “non-integer array index %v”;
ii:访问数组的索引是负数时,报错 “invalid array index %v (index must be non-negative)";
iii:访问数组的索引越界时,报错 “invalid array index %v (out of bounds for %d-element array)";
(2)对于使用变量去访问数组这种在编译期间无法查出错误的情况,Go语言会在运行时阻止其非法访问。(源码那块的实现暂时还没有看懂)对数组的访问和赋值需要同时依赖编译器和运行时,大多数操作在编译期间都会转换成直接读写内存,在中间代码生成期间,编译器还会插入运行时方法 runtime.panicIndex 调用防止发生越界错误。
4.多维数组
与一维数组的操作方式大同小异,没什么讲的
5、数组在函数间的传递,最好传递数组的地址,以指向同类型的一个数组的指针的方式接收,但是要注意如果改变指针指向的值,那么会改变原数组的值。
二.切片
1、概念
1.1 切片是一种是一种数据结构,便于管理和使用数据集合。切片是围绕着动态数组的概念来建立的,可以使用append函数来增长,也可以对切片再次裁剪来缩小它。
1.2 切片在底层的描述
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
在golang里,切片拥有三个字段,uintptr类型(一个整型,足够保证地址的大小范围,32位下4字节,64位下8字节)的Data,其指向一块内存空间,Len表示该切片的长度,Cap表示该切片的容量。其底层和数组一样都是一块连续的存储空间。其实可以这样理解,切片就是对底下数组的一层包装,是对数组的一种引用,我们可以在运行时修改切片的长度和范围,当底层的数组不够时就会触发它的扩容机制。
2、初始化
2.1 使用下标创建
newslice := array[i,j] / oldslice[i,j]
对于底层数组容量为k的数组来说,len=j-i;cap=k-j。这种方法于是最接近底层的一种。例如:slice := array[1 , 3],编译器会将该语句转换为一种 OpSliceMake操作,大体如下:
sliceMake <[ ]type> v1 v2 v3
name &array[*[n]type]: v1
name slice.ptr[*type]: v1
name slice.Len[int]: v2
name slice.Cap[int]: v3
我的理解是这样的,可能不是准确的,请大家解惑一下。对于新的slice构造,得传入type,以及数组指针(可能已经偏移过了)、切片容量以及大小。
2.2 使用make创建
slice := make([ ]type, n)
slice :=make([ ]type, n, m) //容量可选
如果用第一种make方式创建,那么切片的容量和长度相同,用第二种方式创建,容量不能小于长度。
(1)make创建切片很多时候都需要运行时参与工作,如果容量很小就可以直接在栈上或者静态区初始化,如果比较大或发生切片逃逸(切片逃逸是内存逃逸的一种),则在运行时会在堆上初始化
2.3 字面量方式创建
slice := [ ]int{1, 2, 3, 4, 5}
使用字面量方式创建的切片会在编译期间展开,例如 slice := [ ]int{1,2,3,4,5}
var vstat [5]int
vstat[0] = 1
vstat[1] = 2
vstat[2] = 3
vstat[3] = 4
vstat[4] = 5
var vauto *[5]int = new([5]int)
*vauto = vstat
slice := vauto[:]
展开步骤:
(1)根据切片中元素的数量对底层数组的大小进行推导并创建数组。
(2)将字面量的值一一存储到初始化的数组中。
(3)创建一个同样类型大小的指针。
(4)将静态区存储的数组赋值给vauto指针所指向区域
(5)使用[ : ]操作获得一个数组。(其实这一步就可以回到2.1节看一看了,底层传递类型、数组指针、长度、容量。感觉好像形成了一个闭环)
3.操作
3.1 访问
对切片的访问和对数组的访问很像,下标访问会在编译期间转化为对地址的直接访问。
3.2 追加和扩容
3.2.1 容量足够时
(1)没有自己给自己追加
// append(slice, 1, 2, 3)
ptr, len, cap := slice
newlen := len + 3
if newlen > cap {
ptr, len, cap = growslice(slice, newlen)
newlen = len + 3
}
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
return makeslice(ptr, newlen, cap)
如果容量足够大时候,len代表长度,就行c语言中arr[ n]—>*(arr+n),切片在原有的基础上使用后面的没有被使用的位置存放元素。
(2)自己给自己追加
//slice = append(slice, 1, 2, 3)
a := &slice //别忘了切片的结构体是什么样子的了,
ptr, len, cap := slice
newlen := len + 3
if uint(newlen) > uint(cap) {
newptr, len, newcap = growslice(slice, newlen)
vardef(a)
*a.cap = newcap
*a.ptr = newptr
}
newlen = len + 3
*a.len = newlen
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
其实也没有太大的不同
3.2.2 容量不足时
(1)扩容流程:分配新空间,拷贝旧数据,返回新切片
(2)扩容策略:
如果期望容量大于当前容量的两倍就会使用期望容量;
如果当前切片的长度小于 1024 就会将容量翻倍;
如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;