1、切片介绍
Gosliceslicesliceappendslicesliceslice
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 数组指针
len int // 长度
cap int // 容量
}
slice
sliceslice
35int
s := make([]int, 3, 4)
fmt.Println(a, len(s), cap(s)) // [0 0 0] 3 5
2、声明和初始化
Go
是否提前知道切片所需的容量通常会决定如何创建切片
2.1 make创建
// 创建一个整型切片, 其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)
// 我们也可以省略容量, 默认长度==容量
// 创建一个整型切片 其长度和容量都是 5 个元素
slice := make([]int, 5)
// 但是长度不能小于容量, 否则编译器过不了
// a := make([]int, 5, 3)
2.2 字面量创建
// 这种方法和创建数组类似,只是不需要指定[]运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定
slice := []int{1,2,3}
// 和数组一样也可以通过指定索引初始化, 比如index 4 值为100
slice := []int{3: 100}
2.3 创建数组和切片的区别
[]
a := [3]int{1,2,3}
b := []int{1,2,3}
虽然他们声明时只要这一点点区别,但是他们的数据结构区差别却很大,一个是引用类型一个是值类型
2.4 创建切片的本质
makemakeslice
// 这里一波操作过后返回的是slice的pointer
func makeslice(et *_type, len, cap int) unsafe.Pointer {}
3、切片访问
[]
s := []int{1,2,3}
s[0]
// 但是不能越界访问, 比如
s[3] // panic: runtime error: index out of range [3] with length 3
len
cap
4、nil和空切片
nil
var s []int
var s1 []int
fmt.Printf("%pn", s1) // 0x0
make
s := make([]int,0)
// unsafe.Pointer ——> *slice
s2 := make([]int, 0)
fmt.Printf("%pn", s2) // 0x126c9
nil
var s []int
s[0] = 1 // panic: runtime error: index out of range [0] with length 0
5、切片中添加元素
append
s := make([]int, 0, 4)
s = append(s, 10, 20, 30, 40)
现在底层数组已经满了,再往里面追加元素会如何?
s = append(s,50)
append()102410241.2525%
因此扩容对于切片来说是一个比较消耗成本的事情,会开辟新的内存空间
gc
s1 := make([]int, 0, 4)
s1 = append(s1, 10, 20, 30, 40) // 10, 20, 30, 40
fmt.Println(s1, len(s1), cap(s1)) // [10 20 30 40] 4 4
s1 = append(s1, 50)
fmt.Println(s1, len(s1), cap(s1)) // [10 20 30 40 50] 5 8
6、通过切片创建切片
切片之所以被称为切片,是因为创建一个新的切片,也就是把底层数组切出一部分。通过切片创建新切片的语法如下, 详情请参考: 切片的语法
slice[low : high]
slice[low : high : max]
slichigh-lowmax-low
比如
s1 := []int{1, 2, 3, 4}
s2 := s1[2:4:4] // [index2, index4) 左闭右开区间, 容量 4-2
fmt.Println(s2, len(s2), cap(s2)) // [3 4] 2 2
high == maxmax
s3 := s1[2:4]
再次基础上还要几种省略写法:
index 0lenlen
slice[i:] // 从 i 切到最尾部
slice[:j] // 从最开头切到 j(不包含 j)
slice[:] // 从头切到尾,等价于复制整个 slice
注意: 通过切片创建出来的切片是共享底层数据结构的(数组)
共享底层数组会导致相互影响, 比如修改原切片会影响多所有复制出来的切片
s1 := []int{10, 20, 30, 40}
s2 := s1[1:3]
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s1[1], s2[0])
s1[1] = 200
fmt.Println(s1[1], s2[0])
有扩容的原理也可以知道,当扩容后,就不共享底层数组了,比如:
s1 := []int{10, 20, 30, 40}
s2 := s1[1:3:3]
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s1[1], s2[0])
s2 = append(s2, 30) // s2 扩容
s1[1] = 200 // 修改s1
fmt.Println(s1[1], s2[0]) // s1修改并不会影响s2
因此,一般不要修改切片,如果要修改请使用后面的深拷贝复制一个全新的切片
7、切片遍历
Gorangefor
func TestSliceAppend1(t *testing.T) {
s := make([]int, 0, 4)
s = append(s, 10, 20, 30, 40)
for i, v := range s {
fmt.Println(i, v)
}
/*
0 10
1 20
2 30
3 40
*/
}
这种方式底层的实现,也是拷贝一份切片提供给循环使用,因此同样会带来开销
rangerange
8、切片拷贝
不能像数组一样直接使用赋值语句来拷贝一个切片,因为数组是值,而切片是指针, 真正的数据维护在底层数组里面
a1 := [2]{1,2}
a2 := a1 // 值拷贝, a1, a2 互不影响
s1 := []{1, 2}
s2 := s1 // 指针拷贝 s1, s2 指向同一*slice结构体, 就是一个东西,等于没拷贝
Gocopy()
func copy(dst, src []Type) int
srcdstsrcdstsrcdstsrc
s1 := []int{10, 20, 30, 40}
s2 := make([]int, 5)
num := copy(s2, s1) // 这时候s1 和 s2 就是2个切片,包含底层数据, 互不影响
fmt.Println(num) // 4
fmt.Println(s1, s2) // [10 20 30 40] [10 20 30 40 0]
s1[0] = 100
fmt.Println(s1[0], s2[0]) // 100 10
9、切片作为函数参数
函数在调用传参时,都是值拷贝
切片的本质是指针,如果是切片作为函数的参数调用,则拷贝的是指针的地址
因此切片作为函数的参数时,最大的好处是传递效率高
因此切片的用法远多于数组,数组用来定义底层的数据结构
func TestSliceMain2(t *testing.T) {
s1 := make([]int, 0, 4)
s1 = append(s1, 10, 20, 30, 40) // 10, 20, 30, 40
fmt.Println(Sum1(s1)) // 100
}
func Sum1(args []int) int {
sum := 0
for _, v := range args {
sum += v
}
return sum
}