目录
正文
sliceslice[]TTslicesliceslice
数组和切片的区别
数组有确定的长度,而切片的长度不固定,并且可以自动扩容。
数组的定义
go 中定义数组的方式有如下两种:
- 指定长度:
arr := [3]int{1, 2, 3}
- 不指定长度,由编译器推导出数组的长度:
arr := [...]{1, 2, 3}
上面这两种定义方式都定义了一个长度为 3 的数组。正如我们所见,长度是数组的一部分,定义数组的时候长度已经确定下来了。
切片的定义
切片的定义方式跟数组很像,只不过定义切片的时候不用指定长度:
s := []int{1, 2, 3}
在上面定义切片的代码中,我们可以看到其实跟数组唯一的区别就是少了个长度。 那其实我们可以把切片看作是一个无限长度的数组。 当然,实际上它并不是无限的,它只是在切片容纳不下新的元素的时候,会自动进行扩容,从而可以容纳更多的元素。
数组和切片的相似之处
正如我们上面看到的那样,数组和切片两者其实非常相似,在实际使用中,它们也是有些类似的。
比如,通过下标来访问元素:
arr := [3]int{1, 2, 3} // 通过下标访问 fmt.Println(arr[1]) // 2 s := []int{1, 2, 3} // 通过下标访问 fmt.Println(s[1]) // 2
数组的局限
我们知道了,数组的长度是固定的,这也就意味着如果我们想往数组里面增加一个元素会比较麻烦, 我们需要新建一个更大的数组,然后将旧的数据复制过去,然后将新的元素写进去,如:
// 往数组 arr 增加一个元素:4 arr := [3]int{1, 2, 3} // 新建一个更大容量的数组 var arr1 [4]int // 复制旧数组的数据 for i := 0; i < len(arr); i++ { arr1[i] = arr[i] } // 加入新的元素:4 arr1[3] = 4 fmt.Println(arr1)
这样一来就非常的繁琐,如果我们使用切片,就可以省去这些步骤:
// 定义一个长度为 3 的数组 arr := [3]int{1, 2, 3} // 从数组创建一个切片 s := arr[:] // 增加一个元素 s = append(s, 4) fmt.Println(s)
因为数组固定长度的缺点,实际使用中切片会使用得更加普遍。
重新理解 slice
slicesliceslicesliceslice
A1~7sliceBABABABAABABBA
slice 的内存布局
现在假设我们有如下代码:
// 创建一个切片,长度为 3,容量为 7 var s = make([]int, 3, 7) s[0] = 1 s[1] = 2 s[2] = 3 fmt.Println(s)
对应的内存布局如下:
说明:
slicelencapslicearraylen30~2cap
切片容量存在的意义
slice
比如,假如我们有一个切片,然后我们知道需要往它里面存放 1w 个元素, 如果我们不指定容量的话,那么切片就会在它存放不下新的元素的时候进行扩容, 这样一来,可能在我们存放这 1w 个元素的时候需要进行多次扩容, 这也就意味着需要进行多次的内存分配。这样就会影响应用的性能。
我们可以通过下面的例子来简单了解一下:
// Benchmark1-20 100000000 11.68 ns/op func Benchmark1(b *testing.B) { var s []int for i := 0; i < b.N; i++ { s = append(s, 1) } } // Benchmark2-20 134283985 7.482 ns/op func Benchmark2(b *testing.B) { var s []int = make([]int, 10, 100000000) for i := 0; i < b.N; i++ { s = append(s, 1) } }
sliceslice
最终我们发现,在给切片提前设置容量的情况下,会有一定的性能提升。
切片常用操作
创建切片
我们可以从数组或切片生成新的切片:
end
target[start:end]
说明:
targetstartend
如:
s := []int{1, 2, 3} s1 := s[1:2] // 包含下标 1,不包含下标 2 fmt.Println(s1) // [2] arr := [3]int{1, 2, 3} s2 := arr[1:2] fmt.Println(s2) // [2]
start
arr := [3]int{1, 2, 3} fmt.Println(arr[:2]) // [1, 2]
starttarget
end
arr := [3]int{1, 2, 3} fmt.Println(arr[1:]) // [2, 3]
endstarttarget
除此之外,我们还可以指定新的切片的容量,通过如下这种方式:
target[start:end:cap]
例子:
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} s := arr[1:4:5] fmt.Println(s, len(s), cap(s)) // [2 3 4] 3 4
往切片中添加元素
我们前面说过了,如果我们想往数组里面增加元素,那么我们必须开辟新的内存,将旧的数组复制过去,然后才能将新的元素加入进去。
append
var a []int a = append(a, 1) // 追加1个元素 a = append(a, 1, 2, 3) // 追加多个元素 a = append(a, []int{1,2,3}...) // 追加一个切片
切片复制
copy
copy(dst, src []int)
dstsrccopysrcdst
示例:
var a []int var b []int = []int{1, 2, 3} // a 的容量为 0,容纳不下任何元素 copy(a, b) fmt.Println(a) // [] a = make([]int, 3, 3) // 给 a 分配内存 copy(a, b) fmt.Println(a) // [1 2 3]
dstsrcsrc
从切片删除元素
虽然我们往切片追加元素的操作挺方便的,但是要从切片删除元素就相对麻烦一些了。go 语言本身没有提供从切片删除元素的方法。 如果我们要删除切片中的元素,只有构建出一个新的切片:
对应代码:
var a = make([]int, 7, 7) for i := 0; i < 7; i++ { a[i] = i + 1 } fmt.Println(a) // [1 2 3 4 5 6 7] var b []int b = append(b, a[:2]...) // [1 2] b = append(b, a[5:]...) // [1 2 6 7] fmt.Println(b) // [1 2 6 7]
a3、4、52~435
3、4、5
切片的容量到底是多少?
假设我们有如下代码:
var a = make([]int, 7, 7) for i := 0; i < 7; i++ { a[i] = i + 1 } // [1 2 3 4 5 6 7] fmt.Println(a) s1 := a[:3] // [1 2 3] 3 7 fmt.Println(s1, len(s1), cap(s1)) s2 := a[4:6] // [5 6] 2 3 fmt.Println(s2, len(s2), cap(s2))
s1s2
s1arrays256s1s25
s1s2s[start:end]start
切片可以共享底层数组
s1s2s10~2s24~5
s1s2a
var a = make([]int, 7, 7) for i := 0; i < 7; i++ { a[i] = i + 1 } // [1 2 3 4 5 6 7] fmt.Println(a) s1 := a[:3] // [1 2 3] fmt.Println(s1) s1[1] = 100 // [1 100 3 4 5 6 7] fmt.Println(a) // [1 100 3] fmt.Println(s1)
s1as1a
切片扩容不会影响原切片
上一小节我们说了,切片可以共享底层数组。但是如果切片扩容的话,那就是一个全新的切片了。
var a = []int{1, 2, 3} // [1 2 3] 3 3 fmt.Println(a, len(a), cap(a)) // a 容纳不下新的元素了,会进行扩容 b := append(a, 4) // [1 2 3 4] 4 6 fmt.Println(b, len(b), cap(b)) b[1] = 100 // [1 2 3] fmt.Println(a) // [1 100 3 4] fmt.Println(b)
a3ba
下面的例子就不一样了:
// 长度为 2,容量为 3 var a = make([]int, 2, 3) a[0] = 1 a[1] = 2 // [1 2] 2 3 fmt.Println(a, len(a), cap(a)) // a 还可以容纳新的元素,不用扩容 b := append(a, 4) // [1 2 4] 3 3 fmt.Println(b, len(b), cap(b)) b[1] = 100 // [1 100] fmt.Println(a) // [1 100 4] fmt.Println(b)
a3ab := append(a, 4)ba
所以,我们需要尤其注意代码中作为切片的函数参数,如果我们希望在被调函数中修改了切片之后,在 caller 里面也能看到效果的话,最好是传递指针。
func test1(s []int) { s = append(s, 4) } func test2(s *[]int) { *s = append(*s, 4) } func TestSlice(t *testing.T) { var a = []int{1, 2, 3} // [1 2 3] 3 3 fmt.Println(a, len(a), cap(a)) test1(a) // [1 2 3] 3 3 fmt.Println(a, len(a), cap(a)) var b = []int{1, 2, 3} // [1 2 3] 3 3 fmt.Println(b, len(b), cap(b)) test2(&b) // [1 2 3 4] 4 6 fmt.Println(b, len(b), cap(b)) }
test1test1TestSliceatest2test2TestSliceb
总结
[2]int{1, 2}[...]int{1, 2}0 ~ len(x)-1xslicearraylencaparray[1:3]slice[1:3]appendcopyslice[start:end]cap(slice) - startstart