Go 语言的切片(slice)是对数组的抽象概念、是一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合 Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大 切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append() 来实现的,这个函数可以快速且高效地增长切片,也可以通过对切片再次切割,缩小一个切片的大小。因为切片的底层也是在连续的内存块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处
切片的内部实现
切片是一个很小的对象,它对底层的数组(内部是通过数组保存数据的)进行了抽象,并提供相关的操作方法。切片是一个有三个字段的数据结构,这些数据结构包含 Golang 需要操作底层数组的元数据
package main
import "fmt"
func main() {
var slice []int = make([]int, 3, 5)
// arr := []int{10,20,30}
// copy(slice, arr)
slice[0] = 10
slice[1] = 20
slice[2] = 30
fmt.Printf("len=%d cap=%d slice=%v",len(slice),cap(slice),slice)
}
/*
len=3 cap=5 slice=[10 20 30]
*/
- slice的确是一个引用类型
- slice从底层来说,其实是个数据结构(struct结构体)
type slice struct{
ptr *[2]int //指针
len int //长度
cap int //容量
}
切片的定义、创建、初始化
1. 定义切片
你可以声明一个未指定大小的数组来定义切片,切片可以不需要说明长度:
var identifier []T
2. 通过 make() 函数创建切片
var slice []T = make([]T, len) //定义后赋值
slice := make([]T, len) //简写
//简单的示例
slice := make([]int, 5)
也可以指定可选参数容量capacity
slice := make([]T, len, capacity)
//简单示例:创建一个整型切片,其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)
⚠️ 这里 len 是数组的长度并且也是切片的初始长度,Golang 不允许创建容量小于长度的切片,当创建的切片容量小于长度时会在编译时刻报错: len larger than cap in make([]int)
3. 通过字面量创建切片
直接初始化切片,[]int表示是int的切片类型,{1,2,3}为初始化值,其cap=len=3
// 创建字符串切片,其长度和容量都是 3 个元素
myStr := []string{"Jack", "Mark", "Nick"}
// 创建一个整型切片,其长度和容量都是 4 个元素
myNum := []int{10, 20, 30, 40}
当使用切片字面量创建切片时,还可以设置初始长度和容量。要做的就是在初始化时给出所需的长度和容量作为索引。下面的语法展示了如何使用索引方式创建长度和容量都是100个元素的切片:
// 创建字符串切片,使用空字符串初始化第 100 个元素
myStr := []string{99: ""}
区分数组与切片的声明方式
当使用字面量来声明切片时,其语法与使用字面量声明数组非常相似。二者的区别是:如果在 [] 运算符里指定了一个值,那么创建的就是数组而不是切片。只有在 [] 中不指定值的时候,创建的才是切片。看下面的例子:
//创建有 2 个元素的整型数组
var myArray [2]int
myArray := [2]int{1,2}
// 创建长度和容量都是 3 的整型切片
var mySlice []int
mySlice := []int{10, 20, 30}
切片的使用
初始化切片s,是数组arr的引用
s := arr[:]
将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:endIndex]
默认 endIndex 时将表示一直到arr的最后一个元素
s := arr[startIndex:]
默认 startIndex 时将表示从arr的第一个元素开始
s := arr[:endIndex]
通过切片s初始化切片s1
s1 := s[startIndex:endIndex]
通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
s :=make([]int,len,cap)
创建新的切片的本质
让我们通过下面的例子来理解通过切片创建新的切片的本质:
package main
import "fmt"
func main() {
// 创建一个整型切片,其长度和容量都是 5 个元素
myNum := []int{10, 20, 30, 40, 50}
// 创建一个新切片,其长度为 2 个元素,容量为 4 个元素
newNum := myNum[1:3]
fmt.Printf("myNum=%v,len(myNum)=%d,cap(myNum)=%d\n",myNum,len(myNum),cap(myNum))
fmt.Printf("newNum=%v,len(newNum)=%d,cap(newNum)=%d\n",newNum,len(newNum),cap(myNum))
}
/*
myNum=[10 20 30 40 50],len(myNum)=5,cap(myNum)=5
newNum=[20 30],len(newNum)=2,cap(newNum)=5
*/
执行上面的代码后,我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分:
⚠️注意:截取新切片时的原则是 “左含右不含”
- newNum 是从 myNum 的 index=1 处开始截取,截取到 index=3 的前一个元素,也就是不包含 index=3 这个元素。所以,新的 newNum 是由 myNum 中的第2个元素、第3个元素组成的新的切片构,长度为 2,容量为 4
- 切片 myNum 能够看到底层数组全部 5 个元素的容量,而 newNum 能看到的底层数组的容量只有 4 个元素。newNum 无法访问到底层数组的第一个元素。所以,对 newNum 来说,那个元素就是不存在的
共享底层数组的切片
现在两个切片 myNum 和 newNum 共享同一个底层数组。如果一个切片修改了该底层数组的共享
package main
import "fmt"
func main() {
// 创建一个整型切片,其长度和容量都是 5 个元素
myNum := []int{10, 20, 30, 40, 50}
// 创建一个新切片,其长度为 2 个元素,容量为 4 个元素
newNum := myNum[1:3]
newNum[1] = 35
fmt.Println(myNum,"\n",newNum)
}
/*
[10 20 35 40 50]
[20 35]
*/
把 35 赋值给 newNum 索引为 1 的元素的同时也是在修改 myNum 索引为 2 的元素:
切片只能访问到其长度内的元素
上面的代码可以通过编译,但是会产生运行时错误:
panic: runtime error: index out of range
package main
import "fmt"
func main() {
// 创建一个整型切片,其长度和容量都是 5 个元素
myNum := []int{10, 20, 30, 40, 50}
// 创建一个新切片,其长度为 2 个元素,容量为 4 个元素
newNum := myNum[1:3]
// 修改 newNum 索引为 3 的元素,这个元素对于 newNum 来说并不存在
newNum[3] = 45 //panic: runtime error: index out of range [3] with length 2
fmt.Println(myNum,"\n",newNum)
}
/*
[10 20 35 40 50]
[20 35]
*/
切片的遍历
- for 和for range 两种方式 ```go package main import “fmt”
func main() { var arr [5]int = […]int{10, 20, 30, 40, 50} slice := arr[1:] //[20 30 40 50]
for i := 0; i < len(slice); i++ {
fmt.Printf("i=%v v=%v \n", i, slice[i])
}
fmt.Printf("\n—————————————— 分界线 ——————————————\n")
for i, v := range slice {
fmt.Printf("i=%v v=%v \n", i, v)
}
}
/ i=0 v=20 i=1 v=30 i=2 v=40 i=3 v=50 —————————————— 分界线 —————————————— i=0 v=20 i=1 v=30 i=2 v=40 i=3 v=50 /
<a name="g8ma2"></a>
#### len() 和 cap() 函数
切片是可索引的,并且可以由 len() 方法获取长度<br />切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少
```go
package main
import "fmt"
func main() {
var numbers = make([]int,3,5)
printSlice(numbers)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
/*
以上实例运行输出结果为:
len=3 cap=5 slice=[0 0 0]
*/
空(nil)切片
var myNum []int // 创建 nil 整型切片
myNum := make([]int, 0) // 使用 make 创建空的整型切片
myNum := []int{} // 使用切片字面量创建空的整型切片
一个切片在未初始化之前默认为 nil,长度为 0,实例如下:
package main
import "fmt"
func main() {
var numbers []int
if(numbers == nil){
printSlice(numbers)
fmt.Printf("切片是nil的类型的\n")
}
numbers = []int{}
if(numbers != nil){
printSlice(numbers)
fmt.Printf("空切片")
}
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
/*
len=0 cap=0 slice=[]
切片是nil的类型的
len=0 cap=0 slice=[]
空切片
*/
切片截取
可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],实例如下:
package main
import "fmt"
func main() {
// 创建切片
arr := []int{0,1,2,3,4,5,6,7,8}
printSlice(arr)
// 打印原始切片
fmt.Println("arr ==", arr)
// 打印子切片从索引1(包含) 到索引4(不包含
fmt.Println("arr[1:4] ==", arr[1:4])
// 默认下限为
fmt.Println("arr[:3] ==", arr[:3])
// 默认上限为 len(s)
fmt.Println("arr[4:] ==", arr[4:])
arr1 := make([]int,0,5)
printSlice(arr1)
// 打印子切片从索引 0(包含) 到索引 2(不包含)
arr2 := arr[:2]
printSlice(arr2)
// 打印子切片从索引 2(包含) 到索引 5(不包含)
arr3 := arr[2:5]
printSlice(arr3)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
/*
执行以上代码输出结果为:
len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
arr == [0 1 2 3 4 5 6 7 8]
arr[1:4] == [1 2 3]
arr[:3] == [0 1 2]
arr[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]
*/
append() 和 copy() 函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来
下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法
package main
import "fmt"
func main() {
var arr []int
printSlice(arr)
// 允许追加空切片
arr = append(arr, 0)
printSlice(arr)
// 向切片添加一个元素
arr = append(arr, 1)
printSlice(arr)
// 同时添加多个元素
arr = append(arr, 2,3,4)
printSlice(arr)
// 创建切片 arr1 是之前切片的两倍容
arr1 := make([]int, len(arr), (cap(arr))*2)
// 拷贝 arr 的内容到 arr1
copy(arr1,arr)
printSlice(arr1)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
/*
以上代码执行输出结果为:
len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]
*/
append()的扩容高阶讲解
append是一个内置函数,用来在指定的slice后面添加元素(可以多个),并且返回一个新的slice.我们知道每个slice在底层都有一个数组作为支撑,如果在append元素后,修改改了返回的slice,源数组会发生相对应的改变吗?
package main
import "fmt"
func main() {
//案例1
var myNum []int = []int{10, 20, 30, 40, 50}
newNum := myNum[1:3] // [20 30]
newNum = append(newNum, 60) // [20 30 60]
fmt.Printf("myNum=%v newNum=%v cap(newNum)=%v\n",myNum,newNum,cap(newNum))
//案例2
myNum = []int{10, 20, 30, 40} // 创建一个长度和容量都是 4 的整型切片
newNum = append(myNum, 50) //[10 20 30 40 50]
fmt.Printf("myNum=%v newNum=%v cap(newNum)=%v\n",myNum,newNum,cap(newNum))
//案例3
myNum = []int{10, 20, 30, 40} // 创建一个长度和容量都是 4 的整型切片
newNum = myNum[1:3] //[20 30]
newNum = append(newNum, 1,2,3,4,5) //[10 20 30 40 50 1 2 3 4 5]
fmt.Printf("myNum=%v newNum=%v cap(newNum)=%v\n",myNum,newNum,cap(newNum))
}
/*
myNum=[10 20 30 60 50] newNum=[20 30 60] cap(newNum)=4
myNum=[10 20 30 40] newNum=[10 20 30 40 50] cap(newNum)=8
myNum=[10 20 30 40] newNum=[20 30 1 2 3 4 5] cap(newNum)=8
*/
答案 不一定,这要取决slice的底层数组空间。为什么呢?
- 相对于数组而言,使用切片的一个好处是:可以按需增加切片的容量。
- Golang 内置的 append() 函数会处理增加长度时的所有操作细节。要使用 append() 函数,需要一个被操作的切片和一个要追加的值,当 append() 函数返回时,会返回一个包含修改结果的新切片。
- 函数 append() 总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量
- 在上述代码案例1中, 此时因为 newNum 在底层数组里还有额外的容量可用,append() 函数将可用的元素合并入切片的长度,并对其进行赋值。由于和原始的切片共享同一个底层数组,myNum 中索引为 3 的元素的值也被改动了
- 在上述代码案例2中, 如果切片的底层数组没有足够的可用容量,append() 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值,此时 append 操作同时增加切片的长度和容量
- 函数 append() 会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25%的容量(随着语言的演化,这种增长算法可能会有所改变)
限制切片的容量
在创建切片时,使用第三个索引选项引可以用来控制新切片的容量。其目的并不是要增加容量,而是要限制容量。允许限制新切片的容量为底层数组提供了一定的保护,可以更好地控制追加操作
package main
import "fmt"
func main() {
fruit := []string{"Apple", "Orange", "Plum", "Banana", "Grape"} // 创建长度和容量都是 5 的字符串切片
myFruit := fruit[2:3:4] // 将第三个元素切片,并限制容量, 其长度为 1 个元素,容量为 2 个元素
fmt.Printf("fruit=%v len(fruit)=%d cap(fruit)=%d\n",fruit,len(fruit),cap(fruit))
fmt.Printf("myFruit=%v len(myFruit)=%d cap(myFruit)=%d\n",myFruit,len(myFruit),cap(myFruit))
}
/*
fruit=[Apple Orange Plum Banana Grape] myFruit=[Plum]
*/
这个切片操作执行后,新切片里从底层数组引用了 1 个元素,容量是 2 个元素。具体来说,新切片引用了 Plum 元素,并将容量扩展到 Banana 元素:
如果设置的容量比可用的容量还大,就会得到一个运行时错误:
myFruit := fruit[2:3:6]
panic: runtime error: slice bounds out of range
内置函数 append() 在操作切片时会首先使用可用容量。一旦没有可用容量,就会分配一个新的底层数组。这导致很容易忘记切片间正在共享同一个底层数组。一旦发生这种情况,对切片进行修改,很可能会导致随机且奇怪的问题,这种问题一般都很难调查。如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。这样就可以安全地进行后续的修改操作了:
package main
import "fmt"
func main() {
fruit := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
myFruit := fruit[2:3:3]
myFruit = append(myFruit, "Kiwi") // 向 myFruit 追加新字符串
fmt.Printf("myFruit=%v len(myFruit)=%d cap(myFruit)=%d\n",myFruit,len(myFruit),cap(myFruit))
}
/*
myFruit=[Plum Kiwi] len(myFruit)=2 cap(myFruit)=2
*/
这里,我们限制了 myFruit 的容量为 1。当我们第一次对 myFruit 调用 append() 函数的时候,会创建一个新的底层数组,这个数组包括 2 个元素,并将水果 Plum 复制进来,再追加新水果 Kiwi,并返回一个引用了这个底层数组的新切片。因为新的切片 myFruit 拥有了自己的底层数组,所以杜绝了可能发生的问题。我们可以继续向新切片里追加水果,而不用担心会不小心修改了其他切片里的水果。可以通过下图来理解此时内存中的数据结构:
将一个切片追加到另一个切片
内置函数 append() 也是一个可变参数的函数。这意味着可以在一次调用中传递多个值。如果使用 … 运算符,可以将一个切片的所有元素追加到另一个切片里:
package main
import "fmt"
func main() {
// 创建两个切片,并分别用两个整数进行初始化
num1 := []int{1, 2}
num2 := []int{3, 4}
// 将两个切片追加在一起,并显示结果
fmt.Printf("%v\n", append(num1, num2...))
}
/*
[1 2 3 4]
*/
切片知识总结
- 引⽤类型。但⾃⾝是结构体,值拷⻉传递
- 一般使用make()创建
- 使用len()获取元素个数,cap()获取容量
- 属性 len 表⽰可⽤元素数量,读写操作不能超过该限制
- 属性 cap 表⽰最⼤扩张容量,不能超出数组限制
- 如果 slice == nil,那么 len、 cap 结果都等于 0
- 作为变长数组的替代方案,可以关联底层数组的局部或全部
- 可以直接创建或从底层数组获取生成
- 如果多个slice指向相同底层数组,其中一个值的改变会影响全部
- 在通过下标访问元素时下标不能超过len大小,如同数组的下标不能超出len范围一样