切片定义#
func sliceDeclar() {
var s1 []int // 定义存放int类型的切片
var s2 []string // 定义存放string类型的切片
fmt.Println(s1, s2)
fmt.Println(s1 == nil, s2 == nil)
}创建切片和创建数组非常相似,如果在 [] 指定了值,那么创建的是一个数组,反之就是一个切片
创建空切片#
空切片在底层数组包含 0 个元素,也没有分配任何存储空间。一般用于表示空集合。
func createEmptySlice() {
slice1 := []int{}
slice2 := make([]int, 0)
fmt.Println(slice1, slice2)
fmt.Println(slice1 == nil, slice2 == nil)
}基于数组得到切片#
func createSliceByArray() {
arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(arr)
fmt.Println(arr[0:4]) // =>[1 2 3 4] 基于数组得到切片,从0开始到第4个结束(不包含4).原则:左包含右不包含
fmt.Println(arr[:4]) // =>[1 2 3 4] 省略第一个参数,默认从0开始
fmt.Println(arr[3:]) // =>[4 5 6 7 8 9] 省略第二个参数,默认到len(a1)结束
fmt.Println(arr[:]) // =>[1 2 3 4 5 6 7 8 9] 两个参数都省略,默认从0开始到len(a1-1)结束
}输出:
[1 2 3 4 5 6 7 8 9]
[1 2 3 4]
[1 2 3 4]
[4 5 6 7 8 9]
[1 2 3 4 5 6 7 8 9]基于切片得到切片#
func createSliceBySlice() {
arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
arr1 := arr[0:4]
fmt.Println(arr1)
fmt.Printf("len(s5):%d cap(s5):%d \n", len(arr1), cap(arr1))
//由切片得到切片
arr2 := arr1[2:4]
fmt.Println(arr2)
fmt.Printf("len(s5):%d cap(s5):%d \n", len(arr2), cap(arr2))
}输出:
[1 2 3 4]
len(s5):4 cap(s5):9
[3 4]
len(s5):2 cap(s5):7 直接创建并初始化#
func createSlice() {
s1 := []int{1, 3, 4, 5, 67, 88}
s2 := []string{"北京", "上海", "山西"}
fmt.Println(s1, s2)
fmt.Println(s1 == nil, s2 == nil)
fmt.Printf("len(s1):%d cap(s1):%d \n", len(s1), cap(s1))
fmt.Printf("len(s2):%d cap(s2):%d \n", len(s2), cap(s2))
}使用 make 创建切片#
以上大部分都是基于数组来创建切片,如果需要动态的创建一个切片,可以使用内置的make()函数,格式如下:
make([]T, size, cap)其中:
T:切片的元素类型size:切片中元素的数量cap:切片的容量
func createSliceByMake() {
slice1 := make([]string, 5)
// 使用make创建一个长度5,容量为10的切片
slice2 := make([]string, 5, 10)
fmt.Println(slice1, slice2)
// fmt.Println(slice2[6]) // 虽然创建的切片对应底层数组的大小为 10,但是不能访问索引值 5 以后的元素,其实相当于底层数组长度是10但是切片只覆盖到了0~5
}需要说明的是,切片对应的底层数组的大小为指定的容量。比如对于上面的例子,指定了 slice2 的容量为 10,那么 slice2 对应的底层数组的大小就是 10。虽然创建的切片对应底层数组的大小为 10,但是不能访问索引值 5 以后的元素,比如:
fmt.Println(slice2[6])输出:
panic: runtime error: index out of range [6] with length 5虽然创建的切片对应底层数组的大小为 10,但是不能访问索引值 5 以后的元素,其实相当于:底层数组长度是10但是切片 slice2 只覆盖到了 0~5。
切片的长度和容量#
arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}示例一:
s1 := arr[3:] // [4 5 6 7 8 9]
fmt.Println(s1)
// 切片的长度是元素的个数,切片的容量是底层数组从切片的第一个元素到最后一个元素,
fmt.Printf("len(s1):%d cap(s1):%d \n", len(s1), cap(s1))输出:
[4 5 6 7 8 9]
len(s1):6 cap(s1):6 示例二:
s2 := arr[4:8] // [5 6 7 8]
fmt.Println(s2)
// 切片的长度是元素的个数,所以len=4,切片的容量是底层数组从切片的第一个元素到最后一个元素,所以这里就是从4到9
fmt.Printf("len(s2):%d cap(s2):%d \n", len(s2), cap(s2))输出:
[5 6 7 8]
len(s2):4 cap(s2):5 切片的本质#
切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度 len 和切片的容量 cap。参考自: 李文周的博客
举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],示意图如下:

切片s2 := a[3:6],示意图如下:

注意:现在两个切片共享同一个底层数组,因为切片的本质就是对底层数组的封装,所以如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到
切片判断是否为空#
切片之间是不能比较的,不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和 nil 比较。 一个 nil 值的切片并没有底层数组,一个 nil 值的切片的长度和容量都是0。但是不能说一个长度和容量都是0的切片一定是 nil ,例如下面的示例:
func compareSlice() {
var q1 []int // len(q1)=0;cap(q1)=0;q1==nil; 没有被初始化所以q1==nil is true
fmt.Printf("len(q1):%d cap(q1):%d q1==nil:%t \n", len(q1), cap(q1), q1 == nil)
q2 := []int{} // len(q2)=0;cap(q2)=0;q2!=nil; 这里是定义了元素为空的数组,所以q2==nil is false
fmt.Printf("len(q2):%d cap(q2):%d q2==nil:%t \n", len(q2), cap(q2), q2 == nil)
q3 := make([]int, 0) // len(q3)=0;cap(q3)=0; q3!=nil; 这里使用了make所以十分分配内存的只不过cap和len都为0而已,所以q3==nil is false
fmt.Printf("len(q3):%d cap(q3):%d q3==nil:%t \n", len(q3), cap(q3), q3 == nil)
}所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。
切片的赋值拷贝#
下面代码演示了拷贝前后两个变量共享底层数组,之前也说过:对一个切片的修改会影响另一个切片的内容,这点需要特别注意。
func shareArraySlice() {
w1 := make([]int, 3) // [0 0 0]
w2 := w1 // 将w1直接赋值给w2,w1和w2共用一个底层数组
w2[0] = 100
fmt.Println(w1) // [100 0 0]
fmt.Println(w2) // [100 0 0]
}切片遍历#
切片的遍历方式和数组是一致的,支持索引遍历和for range遍历。
func traversalSlice() {
slice := make([]int, 3) // [0 0 0]
for i := 0; i < len(slice); i++ {
fmt.Println(i, slice[i])
}
for index, value := range slice {
fmt.Println(index, value)
}
}append#
Go语言的内建函数 append() 可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
func appendSlice() {
slice := make([]int, 3) // 创建切片:[0 0 0]
slice = append(slice, 1) // 切片中添加第一个元素 1
slice = append(slice, 2, 3, 4, 5, 6) // 继续添加元素 2,3,4,5,6
slice2 := []int{7, 8, 9} // 创建新的切片
slice = append(slice, slice2...) // 将新的切片中的元素都放到w3中,这里...代表将slice2中的元素拆分
fmt.Println(slice) // 输出:[0 0 0 1 2 3 4 5 6 7 8 9]
}**注意:**如果使用 append() 切片可以不被初始化,会自动扩容并添加元素。
var s []int
s = append(s, 1, 2, 3)每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在 append() 函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
例如:
func appendDilatationSlice() {
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
}
}输出:
[0] len:1 cap:1 ptr:0xc000012088
[0 1] len:2 cap:2 ptr:0xc0000120d0
[0 1 2] len:3 cap:4 ptr:0xc000010200
[0 1 2 3] len:4 cap:4 ptr:0xc000010200
[0 1 2 3 4] len:5 cap:8 ptr:0xc00000c340
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc00000c340
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc00000c340
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc00000c340
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc00010e080
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc00010e080从上面的结果可以看出:
append()函数将元素追加到切片的最后并返回该切片- 切片
numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍
切片的扩容策略#
可以通过查看 $GOROOT/src/runtime/slice.go 源码,其中扩容相关代码如下:
newcap := old.cap
doublecap := newcap + newcap
// 1.首先判断如果新申请容量(cap)大于2倍的旧容量(old.cap):那么最终容量等于新申请的容量(cap)
if cap > doublecap {
newcap = cap
} else {
// 2.否则判断如果旧切片的长度小于1024,则最终容量等于旧容量(old.cap)x2
if old.len < 1024 {
newcap = doublecap
} else {
// 3. 否则判断如果旧切片长度大于等于1024 ,则最终容量从旧容量(old.cap)开始循环增加原来的1/4,即 newcap=old.cap,for {newcap += newcap/4} 直到最终容量大于等于新申请的容量cap,即newcap >= cap
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}从上面的代码可以看出以下内容:
首先判断如果新申请容量
cap大于2倍的旧容量old.cap,最终容量newcap等于新申请的容量cap否则判断如果旧切片的长度小于
1024,则最终容量newcap等于旧容量old.cap的两倍否则判断如果旧切片长度大于等于
1024,则最终容量newcap从旧容量old.cap开始循环增加原来的1/4,即newcap=old.cap,for {newcap += newcap/4}直到最终容量newcap大于等于新申请的容量cap,即newcap >= cap如果最终容量
cap计算值溢出,则最终容量cap就是新申请容量cap
需要注意:切片扩容会根据切片中元素的类型不同而做不同的处理,比如 int 和 string 类型的处理方式就不一样。
copy#
首先来看一个问题:
a := []int{1, 2, 3, 4, 5}
b := a
fmt.Println(a) // [1 2 3 4 5]
fmt.Println(b) // [1 2 3 4 5]
b[0] = 1000
fmt.Println(a) // [1000 2 3 4 5]
fmt.Println(b) // [1000 2 3 4 5]由于切片是引用类型,所以 a 和 b 其实都指向了同一块内存地址。修改 b 的同时 a 的值也会发生变化。
copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,使用格式如下:
copy(destSlice, srcSlice []T)其中:
srcSlice: 数据来源切片destSlice: 目标切片
举个例子:
c := []int{1, 2, 3, 4, 5}
d := make([]int, 5)
copy(d, c) // 使用copy()函数将切片c1中的元素复制到切片c2
fmt.Println(c) // [1 2 3 4 5]
fmt.Println(d) // [1 2 3 4 5]
c[0] = 1000
fmt.Println(c) // [1000 2 3 4 5]
fmt.Println(d) // [1 2 3 4 5]从切片中删除元素#
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
// 从切片中删除元素
c3 := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素32
c3 = append(c3[:2], c3[3:]...) // 其实这就是利用append的特性修改了切片内容再返回
fmt.Println(c3) // [30 31 33 34 35 36 37]总结:要从切片c3中删除索引为 index 的元素,操作方法是 c3 = append(c3[:index], c3[index+1:]...)