Go 语言切片是对数组的一种抽象。
Go 数组的长度不可改变,在特定场景中就不太适用,Go 中提供了一种灵活,功能强悍的内置类型:切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性 引用数组片段 ,以实现变长方案。
0 <= len(slice) <= len(array)
,其中array是slice引用的数组。var 切片变量名 []类型
,比如 var str []string
、 var arr []int
。切片 Slice 在源码中的数据结构定义如下:
type slice struct {
array unsafe.Pointer //一个指向数组的指针
len int
cap int
}
切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。
总结:
Go 语言中的切片类型是从数组类型基础上发展出来的新类型,当声明一个数组时,不指定该数组长度,则该类型为切片(“动态数组”),切片有自己独立的内部结构字段(len, cap, array pointer),并于其引用的底层数组共用存储空间。
你可以 声明 一个 未指定大小的数组 来定义切片,切片声明时不需要说明长度([]没有声明长度,说明这是一个切片,而不是一个数组。因为数组声明是必须指定长度的。):
var identifier []type
如上这种形式的只声明不初始化,这时切片 默认 初始化为 nil
:len=0 cap=0 slice=[]
之所以为 nil ,是因为 没有分配存储空间。
实例:一个切片在未初始化之前默认为 nil,长度为 0:
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
if numbers == nil {
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 切片被用在很多标准库和内置函数中,描述一个不存在的切片的时候,就需要用到 nil 切片。比如函数在发生异常的时候,返回的切片就是 nil 切片。nil 切片的指针指向 nil。
空切片一般会用来表示一个空的集合。比如数据库查询,一条结果也没有查到,那么就可以返回一个空切片。
如果你想声明一个拥有初始长度或规定容量的切片(可以指定切片的长度和容量),可以使用 make() 函数来创建切片:
var slice1 []type = make([]type, length, capacity)
也可以简写为
slice1 := make([]type, length, capacity)
这里 length 是数组的长度并且也是切片的初始长度。
容量 capacity 为可选参数(可选的意思是可以缺省,如果不指定capacity,则capacity默认等于length)。
make 创建的切片与其底层数组:
实例:缺省 capacity
package main
import "fmt"
func main() {
numbers := make([]int, 3)
printSlice(numbers)
}
func printSlice(x []int) {
fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}
输出结果:
len=3 cap=3 slice=[0 0 0]
如上这样创建切片、不初始化,那么切片被系统自动初始化为 0
。(而不是 nil )
之所以 不是nil ,是因为 make 函数为其 分配了内存空间。
实例:
package main
import "fmt"
func main() {
//1.声明切片
var s1 []int
if s1 == nil {
fmt.Println("s1是空")
} else {
fmt.Println("s1不是空")
}
// 2.make()创建
var s2 []int = make([]int, 0)
var s3 []int = make([]int, 0, 0)
if s2 == nil {
fmt.Println("s2是空")
} else {
fmt.Println("s2不是空")
}
if s3 == nil {
fmt.Println("s3是空")
} else {
fmt.Println("s3不是空")
}
fmt.Println(s1, s2, s3)
// 3.:=
s4 := []int{}
s5 := []int{1, 2, 3}
fmt.Println(s4, s5)
if s4 == nil {
fmt.Println("s4是空")
} else {
fmt.Println("s4不是空")
}
// 4.从数组切片
arr := [5]int{1, 2, 3, 4, 5} //数组
var s6 []int //切片
s6 = arr[1:4]
fmt.Println(s6)
}
输出结果:
s1是空
s2不是空
s3不是空
[] [] []
[] [1 2 3]
s4不是空
[2 3 4]
有四种创建切片的方法:
其中,只有 “常规声明” 却不初始化的切片被系统默认为 nil
(没有内存空间)。用 make() 函数或 := 创建却不初始化的切片为空切片(拥有内存空间,只是没有元素),如果有元素的话会被系统默认初始化为 0 。这是因为 “常规声明” 不会为切片分配存储空间,而其他方法会分配。
空切片和 nil
切片的区别在于,空切片指向的地址不是 nil
,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。
最后需要说明的一点是。不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的。
s := []int{1, 2, 3} //一句代码完成声明和初始化两个工作
直接完成了声明和初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3:len=3 cap=3 slice=[1 2 3]
初始化切片 s,是数组 arr 的引用:
//用数组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]
用数组初始化切片的方法总结:
全局变量:
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} //这是一个数组
var slice0 []int = arr[start:end]
var slice1 []int = arr[:end]
var slice2 []int = arr[start:]
var slice3 []int = arr[:]
var slice4 = arr[:len(arr)-1] //去掉切片的最后一个元素
局部变量:
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0} //这是一个数组
slice5 := arr[start:end]
slice6 := arr[:end]
slice7 := arr[start:]
slice8 := arr[:]
slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
package main
import (
"fmt"
)
func main() {
data := [...]int{0, 1, 2, 3, 4, 5}
s := data[2:4]
s[0] += 100
s[1] += 200
fmt.Println(s)
fmt.Println(data)
}
输出结果:
[102 203]
[0 1 102 203 4 5]
可见:对切片内容的改变实际上改变的是它所引用的数组。
切片就像一个傀儡、一个指针、一个虚构,对它的操作就是对原数组的操作。切片和它所引用的数组是一体的,虽然我们看到的是一个切片,其实它还是底层的数组。它们两者是统一的,你就把切片当成一个原数组的一段就行。
这时有同学就有疑问了,前面的那么多创建切片的方式,并不都是通过引用数组得来的呀,大部分都是直接创建切片的呀?
实例:
package main
import "fmt"
func main() {
s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。
fmt.Println(s1, len(s1), cap(s1))
s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
fmt.Println(s2, len(s2), cap(s2))
s3 := make([]int, 6) // 省略 cap,相当于 cap = len。
fmt.Println(s3, len(s3), cap(s3))
}
输出结果:
[0 1 2 3 0 0 0 0 100] 9 9
[0 0 0 0 0 0] 6 8
[0 0 0 0 0 0] 6 6
从这个实例可以看到,你创建的的确是切片,但是它还是数组的形式呀,并不是其他形式。
所以说切片的本质是数组,但是并不是数组,它只是引用了数组的一段。
你创建了一个切片,系统会自动为你创建一个底层数组,然后引用这个底层数组生成一个切片。
你觉得你操作的是切片本身,但实际上操作的是它所依托的那个底层的数组。
既然访问的还是底层数组,那我们为什么不直接操作数组呢?
这是因为切片长度可变的灵活性:使用 make 动态创建slice,避免了数组必须用常量做长度的麻烦。
比如:切片resize(调整大小)
package main
import (
"fmt"
)
func main() {
var a = []int{1, 3, 4, 5}
fmt.Printf("slice a : %v , len(a) : %v\n", a, len(a))
b := a[1:2]
fmt.Printf("slice b : %v , len(b) : %v\n", b, len(b))
c := b[0:3]
fmt.Printf("slice c : %v , len(c) : %v\n", c, len(c))
}
输出结果:
slice a : [1 3 4 5] , len(a) : 4
slice b : [3] , len(b) : 1
slice c : [3 4 5] , len(c) : 3
其原理仍然是:读写操作实际目标是底层数组。
但是这里用切片的话,它的长度就非常灵活。
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3}
p := &s[2] // *int, 获取底层数组元素指针。
*p += 100
fmt.Println(s)
}
输出结果:
[0 1 102 3]
实例:
package main
import (
"fmt"
)
func main() {
data := [][]int{ //[]int类型的切片
[]int{1, 2, 3}, //初始化值
[]int{100, 200},
[]int{11, 22, 33, 44},
}
fmt.Println(data)
}
输出结果:
[[1 2 3] [100 200] [11 22 33 44]]
可直接修改 struct array/slice 成员:
package main
import (
"fmt"
)
func main() {
d := [5]struct { //结构体数组
x int
}{} //未初始化
s := d[:] //切片
d[1].x = 10
s[2].x = 20
fmt.Println(d)
fmt.Printf("%p, %p\n", &d, &d[0])
}
输出结果:
[{0} {10} {20} {0} {0}]
0xc00000c540, 0xc00000c540
可以使用 len() 方法获取切片长度。
计算切片容量的方法 cap() 可以测量切片最长可以达到多少。
以下为具体实例:
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]
package main
import (
"fmt"
)
func main() {
var a = []int{1, 2, 3}
fmt.Printf("slice a : %v\n", a)
var b = []int{4, 5, 6}
fmt.Printf("slice b : %v\n", b)
c := append(a, b...)
fmt.Printf("slice c : %v\n", c)
d := append(c, 7)
fmt.Printf("slice d : %v\n", d)
e := append(d, 8, 9, 10)
fmt.Printf("slice e : %v\n", e)
}
输出结果:
slice a : [1 2 3]
slice b : [4 5 6]
slice c : [1 2 3 4 5 6]
slice d : [1 2 3 4 5 6 7]
slice e : [1 2 3 4 5 6 7 8 9 10]
package main
import (
"fmt"
)
func main() {
s1 := make([]int, 0, 5)
fmt.Printf("%p\n", &s1)
fmt.Println(s1)
s2 := append(s1, 1)
fmt.Printf("%p\n", &s2)
fmt.Println(s2)
}
输出结果:
0xc000004078
[]
0xc0000040a8
[1]
package main
import (
"fmt"
)
func main() {
data := [...]int{0, 1, 2, 3, 4, 10: 0} //数组
s := data[:2:3]
fmt.Println(s)
fmt.Println(len(s), cap(s))
s = append(s, 100, 200, 300) // 一次 append 三个值,超出 s.cap 限制。
fmt.Println(s, data) // 重新分配底层数组,与原数组无关。
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。
}
输出结果:
[0 1]
2 3
[0 1 100 200 300] [0 1 2 3 4 0 0 0 0 0 0]
0xc00000c570 0xc00004a060
从输出结果可以看出:append
后的 s 被重新分配了底层数组(也就是说 s 的底层数组不再是 data,那么修改 s 的值不会再影响 data,它们不再有关联),并把原数组中的值拷贝到新数组中。这是因为超出了原切片的容量。在上例中,如果只追加一个值,则不会超过 s.cap 限制,也就不会重新分配。
切片的自动扩容策略是这样的:(文章 简单说说go语言Slice的底层实现 通过分析源码对这一点提出了质疑)
通常 以 2 倍容量 进行扩容,并重新分配底层数组(新底层数组的容量也变大)。
如果切片的容量小于 1024 个元素,扩容的时候就翻倍增加容量。一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。
注意:扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的。
所以,在大批量添加数据时,建议 一次性分配足够大的空间 ,以减少内存分配和数据复制开销。或 初始化足够长的 len 属性,改用索引号进行操作。
及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收。
slice中 cap 重新分配规律:
package main
import (
"fmt"
)
func main() {
s := make([]int, 0, 1)
fmt.Println(s)
c := cap(s) //计算容量
fmt.Println(c)
for i := 0; i < 50; i++ {
s = append(s, i) //按理说 append 第2个元素时就超出了cap,这时会重新分配底层数组来扩大cap
if n := cap(s); n > c {
fmt.Printf("cap: %d -> %d\n", c, n)
c = n
}
}
}
输出结果:
[]
1
cap: 1 -> 2
cap: 2 -> 4
cap: 4 -> 8
cap: 8 -> 16
cap: 16 -> 32
cap: 32 -> 64
我们可以发现,通常以 2 倍的 cap 重新分配。
提一嘴哈,如果给切片 append 元素时,不超切片容量就没事,操作的还是原数组:
package main
import (
"fmt"
)
func main() {
data := [...]int{0, 1, 2, 3, 4, 10: 0} //数组
s := data[:2:5] //将切片容量扩大到5
fmt.Println(s)
fmt.Println(len(s), cap(s))
s = append(s, 100, 200, 300) // 一次 append 三个值,这次没超出 s.cap 限制。
fmt.Println(s, data)
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针
}
输出结果:
[0 1]
2 5
[0 1 100 200 300] [0 1 100 200 300 0 0 0 0 0 0]
0xc00004a060 0xc00004a060
切片的拷贝分为2种,一种是浅拷贝,一种是深拷贝。
浅拷贝:源切片和目的切片共享同一底层数组空间,源切片修改,目的切片同样被修改。(赋值符实现)
深拷贝:源切片和目的切片各自都有彼此独立的底层数组空间,各自的修改,彼此不受影响。(使用内置函数copy()函数实现)
以下通过具体实例来说明:
浅拷贝:源切片和目的切片共享同一底层数组空间
package main
import "fmt"
func main(){
slice1 := make([]int, 5, 5)
slice2 := slice1
slice1[1] = 1
fmt.Println(slice1) //[0 1 0 0 0]
fmt.Println(slice2) //[0 1 0 0 0]
}
深拷贝:源切片和目的切片各自都有彼此独立的底层数组空间
package main
import "fmt"
func main() {
slice1 := make([]int, 5, 5)
slice1[0] = 9
fmt.Println(slice1)
slice2 := make([]int, 4, 4)
slice3 := make([]int, 5, 5)
fmt.Println(slice2)
fmt.Println(slice3)
//拷贝
fmt.Println(copy(slice2, slice1)) //4
fmt.Println(copy(slice3, slice1)) //5
//独立修改
slice2[1] = 2
slice3[1] = 3
fmt.Println(slice1) //[9 0 0 0 0 0]
fmt.Println(slice2) //[9 2 0 0]
fmt.Println(slice3) //[9 3 0 0 0]
}
输出结果:
[9 0 0 0 0]
[0 0 0 0]
[0 0 0 0 0]
4
5
[9 0 0 0 0]
[9 2 0 0]
[9 3 0 0 0]
copy 函数的原理:copy 函数在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。
下面的三个实例描述了深拷贝切片的 copy 方法的使用:
如果想 增加切片的容量,我们可以 创建一个新的更大的切片 并 把原切片的内容都拷贝过来。
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
numbers = append(numbers, 0, 1, 2, 3, 4)
printSlice(numbers)
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
printSlice(numbers1)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1, numbers)
printSlice(numbers1)
}
func printSlice(x []int) {
fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}
输出结果:
len=0 cap=0 slice=[]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 0 0 0 0]
len=5 cap=12 slice=[0 1 2 3 4]
package main
import (
"fmt"
)
func main() {
s1 := []int{1, 2, 3, 4, 5}
fmt.Printf("slice s1 : %v\n", s1)
s2 := make([]int, 10)
fmt.Printf("slice s2 : %v\n", s2)
copy(s2, s1)
fmt.Printf("copied slice s1 : %v\n", s1)
fmt.Printf("copied slice s2 : %v\n", s2)
}
输出结果:
slice s1 : [1 2 3 4 5]
slice s2 : [0 0 0 0 0 0 0 0 0 0]
copied slice s1 : [1 2 3 4 5]
copied slice s2 : [1 2 3 4 5 0 0 0 0 0]
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4}
var s1 []int
copy(s1, s)
fmt.Println(s1) // []
fmt.Println(s) // [1 2 3 4]
s1 = make([]int, 2)
count := copy(s1, s)
fmt.Println(s1) // [1 2]
fmt.Println(s) // [1 2 3 4]
fmt.Println(count) // 2
s1[0] = 5
fmt.Println(s1) // [5 2]
fmt.Println(s) // [1 2 3 4]
}
输出结果:
[]
[1 2 3 4]
[1 2]
[1 2 3 4]
2
[5 2]
[1 2 3 4]
从该例可知,copy 函数只会拷贝目标切片的长度个元素,并且 copy 后两个切片是互相没有影响的。
实例 2 和 3 说明 copy 函数在两个 slice 间复制数据,复制长度以 len 小的为准。
package main
import (
"fmt"
)
func main() {
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println("array data : ", data)
s1 := data[8:]
s2 := data[:5]
fmt.Printf("slice s1 : %v\n", s1)
fmt.Printf("slice s2 : %v\n", s2)
copy(s2, s1)
fmt.Printf("copied slice s1 : %v\n", s1)
fmt.Printf("copied slice s2 : %v\n", s2)
fmt.Println("last array data : ", data)
}
输出结果:
array data : [0 1 2 3 4 5 6 7 8 9]
slice s1 : [8 9]
slice s2 : [0 1 2 3 4]
copied slice s1 : [8 9]
copied slice s2 : [8 9 2 3 4]
last array data : [8 9 2 3 4 5 6 7 8 9]
应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存。
import (
"fmt"
)
func main() {
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice := data[:]
for index, value := range slice {
fmt.Printf("inde : %v , value : %v\n", index, value)
}
}
输出结果:
inde : 0 , value : 0
inde : 1 , value : 1
inde : 2 , value : 2
inde : 3 , value : 3
inde : 4 , value : 4
inde : 5 , value : 5
inde : 6 , value : 6
inde : 7 , value : 7
inde : 8 , value : 8
inde : 9 , value : 9
对字符串进行切片操作:
package main
import (
"fmt"
)
func main() {
str := "hello world"
s1 := str[0:5]
fmt.Println(s1)
s2 := str[6:]
fmt.Println(s2)
}
输出结果:
hello
world
现在要改变英文字符串 “Hello world” 中的内容:
package main
import (
"fmt"
)
func main() {
str := "Hello world"
s := []byte(str) //将字符串类型转换成一个切片,中文字符需要用[]rune(str)
fmt.Println(s)
s[6] = 'G'
s = s[:8]
s = append(s, '!')
fmt.Println(s)
str = string(s) //将切片转换成字符串
fmt.Println(str)
}
输出结果:
[72 101 108 108 111 32 119 111 114 108 100]
[72 101 108 108 111 32 71 111 33]
Hello Go!
改变中文字符串的内容:
package main
import (
"fmt"
)
func main() {
str := "你好,世界!hello world!"
s := []rune(str)
s[3] = '够'
s[4] = '浪'
s[12] = 'g'
s = s[:14]
str = string(s)
fmt.Println(str)
}
输出结果:
你好,够浪!hello go
多一嘴:
数组or切片转字符串:
strings.Replace(strings.Trim(fmt.Sprint(array_or_slice), "[]"), " ", ",", -1)
可以通过设置下限及上限来设置截取切片 [lower_bound:upper_bound]
。
截取的内容是下标从 lower_bound
到 upper_bound-1
,示例如下:
package main
import "fmt"
func main() {
/* 创建切片 */
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
printSlice(numbers)
/* 打印原始切片 */
fmt.Println("numbers ==", numbers)
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4])
/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3])
/* 默认上限为 len(s)*/
fmt.Println("numbers[4:] ==", numbers[4:])
numbers1 := make([]int, 0, 5)
printSlice(numbers1)
/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
number2 := numbers[:2]
printSlice(number2)
/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
number3 := numbers[2:5]
printSlice(number3)
}
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]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[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]
package main
import (
"fmt"
)
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
d1 := slice[6:8]
fmt.Println(d1, len(d1), cap(d1))
d2 := slice[:6:8]
fmt.Println(d2, len(d2), cap(d2))
}
输出结果:
[6 7] 2 4
[0 1 2 3 4 5] 6 8
常规切片截取:slice[6:8]
表示从下标第6位到第7位,长度len为2, 最大可扩充长度cap为 4(6到9,到尾部是默认的)。
两个冒号的截取:slice[:6:8]
, slice内容为从 0 到第 5 位,长度 len 为6,最大扩充项 cap 设置为 8(0到7)。
所以说,两个冒号相对于一个冒号多的那个冒号是第二个冒号,这个冒号后的数字用于控制最大容量。
或者说常规切片截取相对于两个冒号的截取少了第二个冒号,省略了,默认第二个冒号后的数字到尾部。
总结:
a[x:y:z]
切片内容是: [x:y]
切片长度,[x:z]
切片容量。长度计算:y-x,容量计算:z-x。