切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,能够按需自动增加和缩小。切片的动态增加是经过内置函数 append 来实现的。这个函数能够快速且高效地增加切片。还能够经过对切片再次切片来缩小一个切片的大小。由于切片的底层内存也是在连续块中分配的,因此切片还能得到索引、迭代以及为垃圾回收优化的好处。 算法
切片是引用类型,指向底层数组,切片语法和数组很像,只是没有长度而已。slice底层是连续内存,动态增长长度其实若是超过底层数组容量,也会从新分配内存。数据库
- 指向底层的数组,做为变长数组的替代方案,能够关联底层数组的局部或所有
- slice 为引用类型,但自身是结构体,值拷贝传递。
- 若是多个slice指向相同底层数组,其中一个的值改变会影响所有
- 能够直接建立或从底层数组获取生成,通常使用make()建立
- make([]T, len, cap) ,其中cap能够省略,则和len的值相同。
- 属性 len 表示可用元素数量,读写操做不能超过该限制。
- 属性 cap 表示最⼤扩张容量,不能超出数组限制。
- 若是 slice == nil,那么 len、 cap 结果都等于 0。
type slice struct { array unsafe.Pointer len int cap int }
切片是有 3 个字段的数据结构 ,这 3 个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片容许增加到的元素个数(即容量)。 express
能够看出,slice是引用类型。时刻注意一点,slice传递的时候也是值拷贝,把slice的指针,长度,容量拷贝过去,那么一样能够去操做底层数组数组
var slice[]int //只声明一个slice,len 和 cap 都是0 var slice0 []int=[]int{1,2,3}//完整声明,字面量初始化 var slcie1=[]int{1,2,3} //直接var 字面量初始化 slice2:=[]int{1,2,3}//函数内部能够用:=替代var 初始化
通常使用make()进行建立 make([]T, len, cap) 指定类型,长度,容量,make()会在按照容量分配内存空间,也就是分配的数组数据结构
slice:=make([]string,5)//使用长度声明一个字符串切片,长度、容量都是5 slice0:=make([]int,3,5)//使用长度和容量声明int切片,长度为3,容量为5
容量小于长度的切片会在编译时报错架构
slice := make([]int, 5, 3)//注意,不容许,
注意:使用make([]int,3,5)建立slice,指定的容量cap 其实就是初始化的底层数组的长度。可是此slice 长度为3,只能操做底层数组的前3个,后两个是不能操做的,可是能够经过append函数添加到此slice中。若是基于这个切片建立新的切片,新切片会和原有切片共享底层数组,也能经过后期操做(如append)来访问多余容量的元素。app
// 建立字符串切片 // 使用空字符串初始化第 100 个元素 slice := []string{99: ""}
记住,若是在[]运算符里指定了一个值,那么建立的就是数组而不是切片。只有不指定值的时候,才会建立切片 。这里字面量声明索引为99的位置为空字符串,因此,长度和容量都是 100 个元素,最少开辟了100个内存空间。函数
(1)经过两个冒号建立切片,slice[x:y:z]
切片实体[x:y]
切片长度len = y-x
,切片容量cap = z-x
性能
data := [...]int{0, 1, 2, 3, 4, 5, 6} //初始化一个数组 slice := data[1:4:5] // [low : high : max] 经过两个冒号建立切片
使用两个冒号[1:4:5] 从数组中建立切片,长度为4-1=3,也就是索引从1到3 的数据(1,2,3),而后,后面是最大是5,即容量是5-1=4,即,建立的切片是长度为从索引为 一、二、3 的切片,底层数组为[ 1,2,3,4] 优化
(2)经过单个冒号,索引建立slice
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} expression slice len cap comment ------------+----------------------+------+-------+--------------------- data[:6:8] [0 1 2 3 4 5] //6 8 省略 low. data[5:] [5 6 7 8 9] //5 5 省略 high、 max。 data[:3] [0 1 2] //3 10 省略 low、 max。 data[:] [0 1 2 3 4 5 6 7 8 9] //10 10 所有省略。
首先说一下,slice和array的区别,其实就是[]中有没有值,有值就是数组,无值就是slice
// 使用 make 建立空的整型切片 slice := make([]int, 0) // 使用切片字面量建立空的整型切片 slice := []int{}
空切片在底层数组包含 0 个元素,也没有分配任何存储空间。想表示空集合时空切片颇有用.
例如,数据库查询返回 0 个查询结果时
// 建立一个整型切片 // 其容量和长度都是 5 个元素 slice := []int{10, 20, 30, 40, 50} // 改变索引为 1 的元素的值 slice[1] = 25
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := s[2:5] // [2 3 4] s2 := s1[2:6:7] // [4 5 6 7] s3 := s2[3:6] // Error。 不能超过父slice的容量。 //索引s[2:5] 表示从索引2开始,到索引4结束,包括2可是不包括5
因为reslice 的slice指向了同一块连续内存空间,因此操做是会相互影响的
// 建立一个整型切片 // 其长度和容量都是 5 个元素 slice := []int{10, 20, 30, 40, 50} // 建立一个新切片 // 其长度是 2 个元素,容量是 4 个元素 {20,30} newSlice := slice[1:3] // 修改 newSlice 索引为 1 的元素 // 同时也修改了原来的 slice 的索引为 2 的元素 newSlice[1] = 35 //最终底层为 {10,35,30,40,50}
相对于数组而言,使用切片的一个好处是,能够按需增长切片的容量。 函数 append 老是会增长新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操做的切片的可用容量。 (也就是,若是容量够直接加,不够从新分配内存,拷贝原数组加)
s := make([]int, 0, 5) fmt.Printf("%p\n", &s) s2 := append(s, 1) fmt.Printf("%p\n", &s2) fmt.Println(s, s2) //输出结果 0xc000004440 0xc000004480 [] [1]
append函数作的事就是更改slice指向底层数组的值
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s := data[:3] s2 := append(s, 100, 200) // 添加多个值。 fmt.Println(data) fmt.Println(s) fmt.Println(s2) //输出----------append函数更改了slice执行底层数组data,增长了s的长度,并将相应位置的值改变 [0 1 2 100 200 5 6 7 8 9] [0 1 2] [0 1 2 100 200]
package main import "fmt" func main(){ data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s := data[:2:3] //长度为1,容量为3 fmt.Printf("len: %d,cap: %d\n",len(s),cap(s)) fmt.Printf("原地址对比\n%p\n%p\n",&data[0],&s[0]) s2 := append(s, 100) // 添加1一个值,未超过容量 fmt.Printf("len: %d,cap: %d\n",len(s2),cap(s2)) fmt.Printf("未超过容量时地址对比\n%p\n%p\n",&data[0],&s2[0]) s3 := append(s2, 200) // 再次添加一个值,超过容量3 fmt.Printf("len: %d,cap: %d\n",len(s3),cap(s3)) fmt.Printf("超过容量时地址对比\n%p\n%p\n",&data[0],&s3[0]) } //输出 len: 2,cap: 3 原地址对比 0xc00008e000 0xc00008e000 len: 3,cap: 3 未超过容量时地址对比 0xc00008e000 0xc00008e000 len: 4,cap: 6 超过容量时地址对比 0xc00008e000 0xc00007e030
data := [...]int{0, 1, 2, 3, 4, 10: 0} s := data[:2:3] s = append(s, 100, 200) // ⼀次 append 两个值,超出 s.cap 限制。 fmt.Println(s, data) // 从新分配底层数组,与原数组无关。 fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。 //输出 [0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0] 0xc00007e030 0xc00004a060
上面代码表示,切片s 的长度为2,容量为3,而s经过append一次性添加两个值,也就是想把s变为长度为4的切片,这超过了s的原始容量,因此,这样append会从新开辟一段内存地址,长度为4,容量为4。能够看大他们的内存地址是不同的。
s := make([]int, 0, 1) c := cap(s) for i := 0; i < 1000000; i++ { s = append(s, i) if n := cap(s); n > c { fmt.Printf("cap: %d -> %d %.2f\n", c, n,float32(n)/float32(c)) c = n } } //输出 cap: 1 -> 2 2.00 cap: 2 -> 4 2.00 cap: 4 -> 8 2.00 cap: 8 -> 16 2.00 cap: 16 -> 32 2.00 cap: 32 -> 64 2.00 cap: 64 -> 128 2.00 cap: 128 -> 256 2.00 cap: 256 -> 512 2.00 cap: 512 -> 1024 2.00 cap: 1024 -> 1280 1.25 cap: 1280 -> 1696 1.33 cap: 1696 -> 2304 1.36 cap: 2304 -> 3072 1.33 cap: 3072 -> 4096 1.33 cap: 4096 -> 5120 1.25 cap: 5120 -> 7168 1.40 cap: 7168 -> 9216 1.29 cap: 9216 -> 12288 1.33 cap: 12288 -> 15360 1.25 cap: 15360 -> 19456 1.27 cap: 19456 -> 24576 1.26 cap: 24576 -> 30720 1.25 cap: 30720 -> 38912 1.27 cap: 38912 -> 49152 1.26 cap: 49152 -> 61440 1.25 cap: 61440 -> 76800 1.25 cap: 76800 -> 96256 1.25 cap: 96256 -> 120832 1.26 cap: 120832 -> 151552 1.25 cap: 151552 -> 189440 1.25 cap: 189440 -> 237568 1.25 cap: 237568 -> 296960 1.25 cap: 296960 -> 371712 1.25 cap: 371712 -> 464896 1.25 cap: 464896 -> 581632 1.25 cap: 581632 -> 727040 1.25 cap: 727040 -> 909312 1.25 cap: 909312 -> 1136640 1.25
从输出看,容量最开始以2倍的方式进行开辟,数据量越大,增加因子会稳定在1.25左右。
一旦元素个数超过 1000,容量的增加因子会设为 1.25左右,也就是会每次增长 25%的容量。随着语言的演化,这种增加算法可能会有所改变。
在大批量添加数据时,建议一次性分配足够大的空间,以减小内存分配和数据复制开销。或初始化足够长的 len 属性,改用索引号进行操做。及时释放再也不使用的 slice 对象,避免持有过时数组,形成 GC 没法回收。
可是若是是要避免多个slice同时操做同一内存出现错误时,就须要将将slice建立为长度=容量,避免append新slice指向同一内存操做数据,具体状况根据实际灵活选择。
// 建立两个切片,并分别用两个整数进行初始化 s1 := []int{1, 2} s2 := []int{3, 4} // 将两个切片追加在一块儿,并显示结果 fmt.Printf("%v\n", append(s1, s2...)) //使用... 就能够 //输出: [1 2 3 4] //就像经过输出看到的那样,切片 s2 里的全部值都追加到了切片 s1 的后面。使用 Printf时用来显示 append 函数返回的新切片的值
// 建立一个整型切片 // 其长度和容量都是 4 个元素 slice := []int{10, 20, 30, 40} // 迭代每个元素,并显示其值 for i, v := range slice { fmt.Printf("Index: %d Value: %d\n", i, v) } //输出 Index: 0 Value: 10 Index: 1 Value: 20 Index: 2 Value: 30 Index: 3 Value: 40
关键字 range 会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份拷贝
// 建立一个整型切片 // 其长度和容量都是 4 个元素 slice := []int{10, 20, 30, 40} // 迭代每一个元素,并显示值和地址 for index, value := range slice { fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index]) } //输出:-----明显看出内存地址不一样,因此这里的v,是拷贝,其实只是一个内存地址不断更新存不一样的拷贝 Value: 10 Value-Addr: C000058058 ElemAddr: C0000560C0 Value: 20 Value-Addr: C000058058 ElemAddr: C0000560C8 Value: 30 Value-Addr: C000058058 ElemAddr: C0000560D0 Value: 40 Value-Addr: C000058058 ElemAddr: C0000560D8
由于迭代返回的变量是一个迭代过程当中根据切片依次赋值的新变量,因此 value 的地址老是相同的。要想获取每一个元素的地址,可使用切片变量和索引值
迭代返回索引和位置很是有用,若是咱们只想用其中一个,能够经过占位符 “_” 操做
slice := []int{10, 20, 30, 40} for _, value := range slice {// 迭代每一个元素,并显示其值,经过占位符,不须要获得索引 fmt.Printf("Value: %d\n", value) } //输出: Value: 10 Value: 20 Value: 30 Value: 40
函数 copy 在两个 slice 间复制数据,复制长以 len 小的为准。两个 slice 可指向同一底层数组,容许元素区间重叠。
s1 := []int{1, 2, 3, 4, 5, 6} s2 := []int{7, 8, 9} fmt.Println(s2) copy(s2, s1) //将s1 copy到 s2, 谁的长度小听谁的,此处只能拷贝前三个 fmt.Println(s2) //输出------长度大的往长度小的复制最大只能复制以小长度为准的位数 [7 8 9] [1 2 3]
还能够指定复制slice 和被复制slice 的位置进行copy,不指定默认从前日后,如上面代码
s1 := []int{0,1, 2, 3, 4, 5, 6} s2 := []int{7, 8, 9} fmt.Println("s1=",s1) fmt.Println("s2=",s2) copy(s1[4:6], s2[1:3]) //将s2指定位置复制到s1指定的位置 fmt.Println("s1=",s1) //输出 s1= [0 1 2 3 4 5 6] s2= [7 8 9] s1= [0 1 2 3 8 9 6]
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s := data[8:] s2 := data[:5] copy(s2, s) // dst:s2, src:s fmt.Println(s2) fmt.Println(data) //输出 [8 9 2 3 4] [8 9 2 3 4 5 6 7 8 9]
在 64 位架构的机器上,一个切片须要 24 字节的内存:指针字段须要 8 字节,长度字段须要8字节,容量字段须要 8 字节。因为与切片关联的数据包含在底层数组里,不属于切片自己,因此将切片复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片自己,不会涉及底层数组 。因此无论多大的slice,值传递都不会影响性能。
多维切片和多维数组差很少,只不过[]没有值
// 建立一个整型切片的切片 slice := [][]int{{10}, {100, 200}}
使用append 给slice[0]进行增加,查看内存变化
// 建立一个整型切片的切片 slice := [][]int{{10}, {100, 200}} // 为第一个切片追加值为 20 的元素 slice[0] = append(slice[0], 20)
以上代码会先增加切片,会为新的整型切片分配新的底层数组,而后将切片复制到外层切片的索引为 0 的元素