1. 特点
slice并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。
- 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
- 切片的长度可以改变,因此,切片是一个可变数组。
- 切片的遍历方式和数组一样,可以用len()求长度。表示可用元素的数量,读写操作不能超过该限制。
- cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array)。其中array是slice引用的数组。
- 切片的定义:var 变量名 []类型,比如:var str []string var arr []int。
- 如果slice等于nil,那么len,cap结果都等于0。
2. 切片源码
切片对应的数据结构定义位于runtime/slice.go
文件中,其定义如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
- array:指针,指向引用数组对应位置。
- len:可用元素个数。
- cap:容量,可放元素个数。
我们在进行切片赋值,传参,截断时,其实是复制一个slice结构体,只不过底层数组是同一个。这就导致了无论是在复制的切片中修改值,还是修改形参切片值,都会修改到原来的切片和引用的数组。
总的来说:切边底层有一个数组(make创建的切片,底层也会先创建一个数组),切片是数组的引用。
3. 创建切片
4. 切片初始化
package main
import "fmt"
//全局变量
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[2:8]
var slice1 []int = arr[0:6] //可以简写为arr[:6]
var slice2 []int = arr[5:10] //可以简写为arr[5:]
var slice3 []int = arr[0:len(arr)] //可以简写为arr[:]
var slice4 []int = arr[0 : len(arr)-1] //去掉最后一个元素
func main() {
fmt.Printf("全局变量arr %v\n", arr)
fmt.Printf("全局变量slice0 %v\n", slice0)
fmt.Printf("全局变量slice1 %v\n", slice1)
fmt.Printf("全局变量slice2 %v\n", slice2)
fmt.Printf("全局变量slice3 %v\n", slice3)
fmt.Printf("全局变量slice4 %v\n", slice4)
fmt.Println("--------------------------")
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr2[2:8]
slice6 := arr2[0:6] //可以简写为arr[:6]
slice7 := arr2[5:10] //可以简写为arr[5:]
slice8 := arr2[0:len(arr)] //可以简写为arr[:]
slice9 := arr2[:len(arr)-1] //去掉最后一个元素
fmt.Printf("局部变量arr %v\n", arr2)
fmt.Printf("局部变量slice5 %v\n", slice5)
fmt.Printf("局部变量slice6 %v\n", slice6)
fmt.Printf("局部变量slice7 %v\n", slice7)
fmt.Printf("局部变量slice8 %v\n", slice8)
fmt.Printf("局部变量slice9 %v\n", slice9)
}
5. 通过make来创建切片
var slice []type = make([]type, len)
var slice []type = make([]type, len, cap)
slice := make([]type, len)
slice := make([]type, len, cap)
- 切片的内存布局
- 读写操作实际目标是底层数组,只需要注意索引号的差别。
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)
}
- 可直接创建slice对象,自动分配底层数组
package main
import "fmt"
func main() {
var s1 []int = []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) //使用make创建,省略cap,相当于cap=len
fmt.Println(s3, len(s3), cap(s3))
}
- 使用make动态创建slice,避免了数组必须用常量的麻烦。还可以用指针直接访问底层数组,退化成普通数组操作。
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4}
p := &s[1] //获得底层数组元素指针
*p += 100
fmt.Println(s)
}
- 至于[][]T,是指元素类型为[]T
package main
import "fmt"
func main() {
data := [][]int{
[]int{1, 2, 3},
[]int{10, 20, 30, 40},
[]int{100, 200},
}
fmt.Println(data)
}
- 可以直接修改struct array/slice成员
package main
import "fmt"
func main() {
data := [5]struct {
x int
}{}
s := data[:]
s[1].x = 10
s[2].x = 20
fmt.Println(data)
fmt.Printf("%p, %p\n", &data, &data[0])
}
6. 用append内置函数操作切片(切片追加)
append:向slice尾部添加数据,返回新的slice对象。
package main
import (
"fmt"
)
func main() {
var s []int //s是nil,没有分配内存
fmt.Println(s)
s = append(s, 1) //append会先为s分配内存,再存放数据
fmt.Println(s)
var a []int = []int{1, 2, 3}
fmt.Printf("slice a: %v\n", a)
b := []int{4, 5, 6}
fmt.Printf("slice b: %v\n", b)
//append向slice尾部添加数据
c := append(a, b...)
fmt.Printf("slice c: %v\n", c)
d := append(a, 10)
fmt.Printf("slice d: %v\n", d)
e := append(a, 100, 200, 300)
fmt.Printf("slice e: %v\n", e)
//append返回的新的slice对象
fmt.Printf("a:%p, c:%p, d:%p, e:%p", &a, &c, &d, &e)
}
8. slice扩容
- 超出元slice.cap限制,就会重新分配底层数组,即便原数组并未填满。
从下面现象可以看出:
- 当数据没有超出slice的cap时,如果引用存在的数组变量(不是make创建的),底层数组使用的是存在的数组变量。修改切片的值,就会修改数组的值。
- 当数据超出slice的cap时,会重新分配底层数组,修改切片的值,不会修改存在数组变量的值,而是修改切片底层数组的值。
package main
import "fmt"
func main() {
var data = [...]int{1, 2, 3, 4, 10: 0}
s := data[:2:3]
fmt.Printf("data:%p, data[0]:%p\n", &data, &data[0]) //data:0xc0000103f0, data[0]:0xc0000103f0
//slice中数据保存的是数组对应下标的地址,使用索引访问,相当于对指针解引用,在取地址,实际取的是数组中的地址
fmt.Printf("s:%p, s[0]:%p\n", &s, &s[0]) //s:0xc000008048, s[0]:0xc0000103f0
//未扩容前修改值
s[0] = 11
fmt.Println(s, data)
s = append(s, 100, 200)
fmt.Println(s, data)
fmt.Printf("s[0]:%p, data[0]:%p\n", &s[0], &data[0])
//扩容后修改值
s[0] = 10
fmt.Println(s, data)
}
- slice中cap重新分配规律
从下面输出可以看出,slice扩容通常是以两倍原来容量重新分配底层数组。
建议一次性分配足够大的空间,以减少内存分配和数据复制的开销,或者初始化足够长的len属性,改用索引号进行操作。
及时释放不再使用的slice对象,避免持有过期数组,造成GC无法回收。
package main
import "fmt"
func main() {
s := make([]int, 0, 2)
c := cap(s)
for i := 0; i < 50; i++ {
s = append(s, i)
if c < cap(s) {
fmt.Printf("cap: %d->%d\n", c, cap(s))
c = cap(s)
}
}
}
9. 切片拷贝
函数copy在两个slice间复制数据,复制长度以len小的为准。两个slice可指向同一底层数组,允许元素区间重叠(本来创建切片不同的slice也可以指向同一个底层数组)。
应及时将所需数据copy到较小的slice,以便释放超大号底层数组内存。
10. slice遍历
slice遍历实际和数组遍历一样。
package main
import "fmt"
func main() {
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := data[:]
for index, value := range s1 {
fmt.Printf("s1[%d] = %d\n", index, value)
}
fmt.Println("-----------------------------")
for i := 0; i < len(s1); i++ {
fmt.Printf("s1[%d] = %d\n", i, s1[i])
}
}
11. 切片调整大小
由于底层数组相同,且数组空间连续。所以可以像下面赋值。
12. 数组和切片的内存分布
底层数组不一定要声明出来,比如:make的切片,或者 s := []int{1, 2, 3}
13. 字符串和切片
string底层就是一个byte的数组,因此,也可以进行切片操作。
package main
import "fmt"
func main() {
str := "hello world"
s := str[:5]
fmt.Println(s)
s1 := str[6:]
fmt.Println(s1)
}
- 修改字符串
string本身是不可修改的,要修改string,需要先转成[]byte或[]rune,进行修改,再转成string。
英文字符串
package main
import "fmt"
func main() {
str := "hello world"
//字符串不能修改
// s1 := str[:8]
// s1[6] = 'G'
// str[6] = 'G'
//转成[]byte
bt := []byte(str) //中文字符需要用[]rune(str)
//修改[]byte
bt[6] = 'G'
bt = bt[:8]
bt = append(bt, '!')
str = string(bt)
fmt.Println(str)
}
含中文字符串
package main
import "fmt"
func main() {
str := "你好 世界 hello world"
//字符串不能修改
// s1 := str[:8]
// s1[6] = 'G'
// str[6] = 'G'
rn := []rune(str)
//修改[]byte
rn[3] = '够'
rn[4] = '浪'
rn = rn[:12]
rn = append(rn, '好')
//再转回字符串
str = string(rn)
fmt.Println(str)
}
- 数组或切片转字符串
strings.Replace(strings.Trim(fmt.Sprint(array_or_slice), "[]"), "", ",", -1)
golang slice data[:6:8]两个冒号的理解:
对于data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
常规slice,data[6:8],从data数组索引6到索引7,长度len为2,最大可扩充长度cap为4(slice底层数组为data,指针指向索引6位置,cap从索引6位置到data最后,即6-9,所以cap为4)。
另一种写法:data[:6:8]每一个数字前面都有冒号,slice内容为data索引从0到6,长度len为6,最大扩充项cap设置为8。
a[x:y:z]切片内容[x:y](前闭后闭),切片长度:y-x,切片容量:z-x
package main
import "fmt"
func main() {
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := data[6:8]
fmt.Println(len(s1), cap(s1))
//s2 := data[6: :8] 中间必须要有数字
s2 := data[:6:8]
fmt.Println(len(s2), cap(s2))
fmt.Println(s2)
}