在学习Go语言的切片时,自己首当其冲联想到了之前学习的其他语言的数组,于是将切片和数组联立学习过后,记录了二者的相同与不同,方便自身以后复习使用。如若有遗漏缺陷也会在后续进行完善和修改。
1.数组
1.1概念和使用
数组的概念:数组是具有相同 唯一类型 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。
所以对于数组的声明,要注意:
- 元素类型及元素个数
- 数组⻓度必须是整数且⼤于 0
- 未初始化的数组不是nil,也就是说没有空数组(与切⽚不同)等
对于一维数组的声明通常情况为:var 变量名 [数组长度] 数据类型 。
而⼆维数组是最简单的多维数组,⼆维数组本质上是由⼀维数组组成的。⼆维数组定义⽅式为:
var 变量名 [行长度][列长度] 数据类型 。
(学java的时候还是习惯new int[]这样小括号在后的感觉)
var buffer [256]byte //举例:这是一个叫buffer的包含256个字节的数组变量,访问buffer只需buffer[num]即可
var a = [3][4]int{
{0, 1, 2, 3} , /* 第⼀⾏索引为 0 */
{4, 5, 6, 7} , /* 第⼆⾏索引为 1 */
{8, 9, 10, 11}, /* 第三⾏索引为 2 */
}
var myValue int = a[2][3] //举例:a为一个二维数组的声明,myValue表示访问⼆维数组 a 第三⾏的第四个元素
当然,也可以使用 [...] 忽略数组的长度,编译器自主决定,但是一定注意:设置了元素个数,{} 中的元素个数就不能超过它。
arr :=[2]{1,2,3} //举例:这是一个超过元素个数的错误数组声明
arr :=[...]{1,2,3} //举例:这是一个省略元素个数的数组声明
2.切片
2.1切片的定义和使用
切片对于Go语言相当于一种对数组的抽象。(或者一种promax?)
与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。(据说是内置了一种append函数来方便扩容)
定义切片
你可以声明一个未指定大小的数组来定义切片:
var identifier []type
切片不需要说明长度。
或使用 make() 函数来创建切片:
var slice1 []type = make([]type, len) //type即为数据类型,len即为长度
//也可以简写为
slice1 := make([]type, len)
注意: 切片如果直接创建是一个长度为0没有元素的切片,用len()和cap()进行长度和容量打印,均显示为0。
关于容量和长度还需注意: 由于之前提到的扩容机制,长度len和容量cap有些许出入,需注意以下几点:
- 通常情况下,初始时创建的切片长度和容量都是相等的。
- 当往切片追加元素时,如果超过了它的容量,则会触发自动扩容机制,并将原始底层数组复制到新分配的更大数组中。
- 在扩展后的新切片中,长度会增加而容量可能会增加或保持不变,具体取决于系统内部运算规则。
- 切片与底层数组共享内存空间,在修改底层数组时所有引用该底层数组的切片都会受影响。
- 通常情况下,在向其他函数传递切片时,建议同时将其长度作为参数进行传递,以便接收方了解切片的大小。
归纳二者的不同点即为切片和底层数组的关系,合理扩容且避免底层数组和切片内存分配错误,即可规避越界的问题
2.2切片的截取和扩容
截取
切片可以像以前学过的字符串的substring()方法直接截取一段数组供使用者使用。
注意: 此处的截取为左开右闭规则,即包含左边下限的索引,直到右边上限索引的前一个为止。
举例:
package main
import "fmt"
func main() {
/* 创建切片 */
numbers := []int{0,1,2,3,4}
printSlice(numbers) //len=5 cap=5 slice=[0 1 2 3 4]
/* 打印原始切片 */
fmt.Println("numbers ==", numbers) //numbers ==[0 1 2 3 4]
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:3] ==", numbers[1:3]) //numbers[1:3] ==[1 2]
/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3]) //numbers[:3] ==[0 1 2]
/* 默认上限为 len(s)*/
fmt.Println("numbers[3:] ==", numbers[3:]) //numbers[4:] ==[3 4]
numbers1 := make([]int,0,3)
printSlice(numbers1) // len=0 cap=3 slice=[]
/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
number2 := numbers[:2]
printSlice(number2) //len=2 cap=5 slice=[0 1]
/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
number3 := numbers[2:5]
printSlice(number3) //len=3 cap=3 slice=[2 3 4]
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
可以发现: 截取后的容量与长度各不相同,长度相当于右边界-左边界而取值,而容量只于左边界有关,起点发生变化才会有底层数组长度-左边界的计算。
扩容——append() 和 copy() 函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来,这里就用到了copy()函数。
而向切片追加新元素,则用到了 append 方法。以下为两者的实例:
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers) //len=0 cap=0 slice=[]
/* 允许追加空切片 */
numbers = append(numbers, 0)
printSlice(numbers) //len=1 cap=1 slice=[0]
/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers) //len=2 cap=2 slice=[0 1]
/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers) //len=5 cap=6 slice=[0 1 2 3 4]
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1) //len=5 cap=12 slice=[0 1 2 3 4]
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
注意: 这里还能发现切片扩容的细节,即如果cap(slice) < len(slice),会根据扩容策略进行扩容,如上面代码中len=5,cap=6即发生了一定扩容。
扩容有主要以下策略:
- 新的底层数组的容量会根据扩容策略确定。一般情况下,Go语言会将新的容量按照原有容量的两倍进行扩展(如果原有容量小于1024);
- 或者按照原有容量的1.25倍进行扩展(如果原有容量大于等于1024)。
扩容完成后,切片会指向新的底层数组,并丢弃原有的底层数组。额外需要注意的是,当切片扩容时,底层数组的地址可能会发生改变。因此,对于保存切片地址的变量和函数参数,扩容后要重新赋值,否则可能会导致潜在的bug。
总结
对于数组和切片个人理解为青出于蓝而胜于蓝,切片不仅仅将数组动态起来,而且提供了截取,扩容和拷贝方法,有一种当初学Java的String遇上String buffer的美 (存疑),使死板的数组能更方便的更改和延长,不再局限于一亩三分地中,所以切片应为主要使用,不过硬性需要数组集合时也请务必选择数组集合。