Go 语言入门指南:自己对于切片的小小记录| 青训营

159 阅读7分钟

在学习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([]intlen(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即发生了一定扩容。

扩容有主要以下策略:

  1. 新的底层数组的容量会根据扩容策略确定。一般情况下,Go语言会将新的容量按照原有容量的两倍进行扩展(如果原有容量小于1024);
  2. 或者按照原有容量的1.25倍进行扩展(如果原有容量大于等于1024)。

扩容完成后,切片会指向新的底层数组,并丢弃原有的底层数组。额外需要注意的是,当切片扩容时,底层数组的地址可能会发生改变。因此,对于保存切片地址的变量和函数参数,扩容后要重新赋值,否则可能会导致潜在的bug。


总结

对于数组和切片个人理解为青出于蓝而胜于蓝,切片不仅仅将数组动态起来,而且提供了截取,扩容和拷贝方法,有一种当初学Java的String遇上String buffer的美 (存疑),使死板的数组能更方便的更改和延长,不再局限于一亩三分地中,所以切片应为主要使用,不过硬性需要数组集合时也请务必选择数组集合。