在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
切片简单介绍slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。 数组是定长的,长度定义好之后,不能再更改。而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。 数组就是一片连续的内存, slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。 // runtime/slice.go type slice struct { array unsafe.Pointer // 元素指针 len int // 长度 cap int // 容量 } slice 的数据结构如下:
切片的创建和初始化在 Golang 中可以通过多种方式创建和初始化切片。是否提前知道切片所需的容量通常会决定如何创建切片。 通过 make() 函数创建切片 // 创建一个整型切片 // 其长度和容量都是 5 个元素 slice := make([]int, 5) 此时只指定了切片的长度,那么切片的容量和长度相等。也可以分别指定长度和容量: // 创建一个整型切片 // 其长度为 3 个元素,容量为 5 个元素 slice := make([]int, 3, 5) 分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,但是初始化后并不能访问所有的数组元素。 注意,Golang 不允许创建容量小于长度的切片,当创建的切片容量小于长度时会在编译时刻报错: // 创建一个整型切片 // 使其长度大于容量 myNum := make([]int, 5, 3) 编译上面的代码,会收到下面的编译错误: len larger than cap in make([]int) 通过字面量创建切片 // 创建字符串切片 // 其长度和容量都是 3 个元素 myStr := []string{"Jack", "Mark", "Nick"} // 创建一个整型切片 // 其长度和容量都是 4 个元素 myNum := []int{10, 20, 30, 40} 当使用切片字面量创建切片时,还可以设置初始长度和容量。要做的就是在初始化时给出所需的长度和容量作为索引。下面的语法展示了如何使用索引方式创建长度和容量都是100个元素的切片: // 创建字符串切片 // 使用空字符串初始化第 100 个元素 myStr := []string{99: ""} 区分数组的声明和切片的声明方式 // 创建有 3 个元素的整型数组 myArray := [3]int{10, 20, 30} // 创建长度和容量都是 3 的整型切片 mySlice := []int{10, 20, 30}
slice基本实现slice结构大致存储了三个部分,第一部分为指向底层数组的指针
arr := [5]int{0,1,2,3,4} slice := arr[1:4] 有一个数组 func main() { arr := [5]int{0, 1, 2, 3, 4} fmt.Println(arr) slice := arr[1:4] slice2 := arr[2:5] fmt.Printf("arr %v, slice1 %v, slice2 %v, %p %p %p\n", arr, slice, slice2, &arr, &slice, &slice2) fmt.Printf("arr[2]%p slice[1] %p slice2[0]%p\n", &arr[2], &slice[1], &slice2[0]) arr[2] = 2222 fmt.Printf("arr %v, slice1 %v, slice2 %v\n", arr, slice, slice2) slice[1] = 1111 fmt.Printf("arr %v, slice1 %v, slice2 %v\n", arr, slice, slice2) } 输出的值为: [0 1 2 3 4] arr [0 1 2 3 4], slice1 [1 2 3], slice2 [2 3 4], 0xc42006e0c0 0xc4200660c0 0xc4200660e0 arr[2]0xc42006e0d0 slice[1] 0xc42006e0d0 slice2[0]0xc42006e0d0 arr [0 1 2222 3 4], slice1 [1 2222 3], slice2 [2222 3 4] arr [0 1 1111 3 4], slice1 [1 1111 3], slice2 [1111 3 4] 由此可见,数组的切片,只是从数组上切一段数据下来,不同的切片,其实是共享这些底层的数据数据。不过这些切片本身是不一样的对象,其内存地址都不一样。 从数组中切一块下来形成切片很好理解,有时候我们用make函数创建切片,实际上golang会在底层创建一个匿名的数组。如果从新的slice再切,那么新创建的两个切片都共享这个底层的匿名数组。 func main() { slice := make([]int, 5) for i:=0; i<len(slice);i++{ slice[i] = i } fmt.Printf("slice %v \n", slice) slice2 := slice[1:4] fmt.Printf("slice %v, slice2 %v \n", slice, slice2) slice[1] = 1111 fmt.Printf("slice %v, slice2 %v \n", slice, slice2) } 输出如下: slice [0 1 2 3 4] slice [0 1 2 3 4], slice2 [1 2 3] slice [0 1111 2 3 4], slice2 [1111 2 3]
package main import "fmt" func main() { slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := slice[2:5] s2 := s1[2:6:7] s2 = append(s2, 100) s2 = append(s2, 200) s1[2] = 20 fmt.Println(s1) fmt.Println(s2) fmt.Println(slice) } 结果: [2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]
接着,向 s2 = append(s2, 100)
再次向 s2 = append(s2, 100)
这时, 最后,修改 s1[2] = 20
这次只会影响原始数组相应位置的元素。它影响不到 再提一点,打印
slice的复制既然slice的创建依赖于数组,有时候新生成的slice会修改,但是又不想修改原来的切片或者数组。此时就需要针对原来的切片进行复制了。 func main() { slice := []int{0, 1, 2, 3, 4} slice2 := slice[1:4] slice3 := make([]int, len(slice2)) for i, e := range slice2 { slice3[i] = e } fmt.Printf("slice %v, slice3 %v \n", slice, slice3) slice[1] = 1111 fmt.Printf("slice %v, slice3 %v \n", slice, slice3) } 输出: slice [0 1 2 3 4], slice3 [1 2 3] slice [0 1111 2 3 4], slice3 [1 2 3] 由此可见,新创建的slice3,不会因为slice和slice2的修改而改变slice3。复制很有用,因此golang实现了一个内建的函数 func main() { slice := []int{0, 1, 2, 3, 4} slice2 := slice[1:4] slice4 := make([]int, len(slice2)) copy(slice4, slice2) fmt.Printf("slice %v, slice4 %v \n", slice, slice4) slice[1] = 1111 fmt.Printf("slice %v, slice4 %v \n", slice, slice4) } slice4是从slice2中copy生成,slice和slice4底层的匿名数组是不一样的。因此修改他们不会影响彼此。
slice 追加append 简介创建复制切片都是常用的操作,还有一个追加元素或者追加数组也是很常用的功能。golang提供了 当添加元素超过容量时时会发生扩容。 func main() { slice := make([]int, 1, 2) slice[0] = 111 fmt.Printf("slice %v, slice addr %p, len %d, cap %d \n", slice, &slice, len(slice), cap(slice)) slice = append(slice, 222) fmt.Printf("slice %v, slice addr %p, len %d, cap %d \n", slice, &slice, len(slice), cap(slice)) slice = append(slice, 333) fmt.Printf("slice %v, slice addr %p, len %d, cap %d \n", slice, &slice, len(slice), cap(slice)) } 输出结果为:
切片容量无论数组还是切片,都有长度限制。也就是追加切片的时候,如果元素正好在切片的容量范围内,直接在尾部追加一个元素即可。如果超出了最大容量,再追加元素就需要针对底层的数组进行复制和扩容操作了。 这里有一个切片容量的概念,从数组中切数据,切片的容量应该是切片的最后一个数据,和数组剩下元素的大小,再加上现有切片的大小。 数组 [0, 1, 2, 3, 4] 中,数组有5个元素。如果切片 s = [1, 2, 3],那么
尽管上面的第二个和第三个切片的长度一样,但是他们的容量不一样。容量与最终append的策略有关系。 append简单实现我们已经知道,切片都依赖底层的数组结构,即使是直接创建的切片,也会生成一个匿名的数组。使用append时候,本质上是针对底层依赖的数组进行操作。如果切片的容量大于长度,给切片追加元素其实是修改底层数中,切片元素后面的元素。如果容量满了,就不能在原来的数组上修改,而是要创建一个新的数组,当然golang是通过创建一个新的切片实现的,因为新切片必然也有一个新的数组,并且这个数组的长度是原来的2倍,使用动态规划算法的简单实现。 func main() { arr := [3]int{0, 1, 2} slice := arr[1:2] fmt.Printf("arr %v len %d, slice %v len %d, cap %d, \n", arr, len(arr), slice, len(slice), cap(slice)) slice[0] = 333 fmt.Printf("arr %v len %d, slice %v len %d, cap %d, \n", arr, len(arr), slice, len(slice), cap(slice)) slice = append(slice, 4444) fmt.Printf("arr %v len %d, slice %v len %d, cap %d, \n", arr, len(arr), slice, len(slice), cap(slice)) slice = append(slice, 5555) fmt.Printf("arr %v len %d, slice %v len %d, cap %d, \n", arr, len(arr), slice, len(slice), cap(slice)) slice[0] = 333 fmt.Printf("arr %v len %d, slice %v len %d, cap %d, \n", arr, len(arr), slice, len(slice), cap(slice)) } 输出:
小于容量的append重输出,我们来画一下这个动态过程的图示:
arr 是一个含有三个元素的数组,slice从arr中切了一个元素,由于切片的最后一个元素 超出容量的append如果接着append一个元素,那么数组肯定越界。此时append的原理大致如下:
上面的图示描述了大于容量的时候append的操作原理。新生成的切片其依赖的数组和原来的数组就没有关系了,因此在修改新的切片元素,旧的数组也不会有关系。至于临时的切片t,将会被golang的gc回收。当然arr或它衍生的切片都没有应用的时候,也会被gc所回收。
作为函数参数的切片直接改变切片切片传递的时候,传的是数组的值,等效于从原始切片中再切了一次。原始切片slice和参数s切片的底层数组是一样的。因此修改函数内的切片,也就修改了数组。
例如下面的代码: slice := make([]int, 2, 3) for i := 0; i < len(slice); i++ { slice[i] = i } fmt.Printf("slice %v %p \n", slice, &slice) ret := changeSlice(slice) fmt.Printf("slice %v %p, ret %v \n", slice, &slice, ret) ret[1] = 1111 fmt.Printf("slice %v %p, ret %v \n", slice, &slice, ret) } func changeSlice(s []int) []int { fmt.Printf("func s %v %p \n", s, &s) s = append(s, 3) return s } 输出:
从输出可以看出,当slice传递给函数的时候,新建了切片s。在函数中给s进行了append一个元素,由于此时s的容量足够到,并没有生成新的底层数组。当修改返回的ret的时候,ret也共用了底层的数组,因此修改ret的原始,相应的也看到了slice的改变。 append 操作如果在函数内,append操作超过了原始切片的容量,将会有一个新建底层数组的过程,那么此时再修改函数返回切片,应该不会再影响原始切片。例如下面代码: func main() { slice := make([]int, 2, 2) for i := 0; i < len(slice); i++ { slice[i] = i } fmt.Printf("slice %v %p \n", slice, &slice) ret := changeSlice(slice) fmt.Printf("slice %v %p, ret %v \n", slice, &slice, ret) ret[1] = -1111 fmt.Printf("slice %v %p, ret %v \n", slice, &slice, ret) } func changeSlice(s []int) []int { fmt.Printf("func s %v %p \n", s, &s) s[0] = -1 s = append(s, 3) s[1] = 1111 return s } 输出:
从输出可以很清楚的看到了我们的猜想。 即函数中先改变s第一个元素的值,由于slice和s都共用了底层数组,因此无论原始切片slice还是ret,第一个元素都是-1.然后append操作之后,因为超出了s的容量,因此会新建底层数组,虽然s变量没变,但是他的底层数组变了,此时修改s第一个元素,并不会影响原始的slice切片。也就是slice[1]还是1,而ret[1]则是-1。最后在外面修改ret[1]为 -1111,也不会影响原始的切片slice。 通过上面的分析,我们大致可以下结论,slice或者array作为函数参数传递的时候,本质是传值而不是传引用。传值的过程复制一个新的切片,这个切片也指向原始变量的底层数组。(个人感觉称之为传切片可能比传值的表述更准确)。函数中无论是直接修改切片,还是append创建新的切片,都是基于共享切片底层数组的情况作为基础。也就是最外面的原始切片是否改变,取决于函数内的操作和切片本身容量。 传引用方式array和slice作为参数传递的过程基本上是一样的,即传递他们切片。有时候我们需要处理传递引用的形式。golang提供了指针很方便实现类似的功能。 func main() { slice := []int{0, 1} fmt.Printf("slice %v %p \n", slice, &slice) changeSlice(&slice) fmt.Printf("slice %v %p \n", slice, &slice) slice[1] = -1111 fmt.Printf("slice %v %p \n", slice, &slice) } func changeSlice(s *[]int) { fmt.Printf("func s %v %p \n", *s, s) (*s)[0] = -1 *s = append(*s, 3) (*s)[1] = 1111 } 输出如下:
从输出可以看到,传递给函数的是slice的指针,函数内对对s的操作本质上都是对slice的操作。并且也可以从函数内打出的s地址看到,至始至终就只有一个切片。虽然在append过程中会出现临时的切片或数组。 总结golang提供了array和slice两种序列结构。其中array是值类型。slice则是复合类型。slice是基于array实现的。slice的第一个内容为指向数组的指针,然后是其长度和容量。通过array的切片可以 因为slice依赖其底层的array,修改slice本质是修改array,而array又是有大小限制,当超过slice的容量,即数组越界的时候,需要通过动态规划的方式创建一个新的数组块。把原有的数据复制到新数组,这个新的array则为slice新的底层依赖。 数组还是切片,在函数中传递的不是引用,是另外一种值类型,即通过原始变量进行 本文记录整理:
https://github.com/qcrao/Go-Questions/tree/master/%E6%95%B0%E7%BB%84%E5%92%8C%E5%88%87%E7%89%87
https://www.jianshu.com/p/354fce23b4f0
https://studygolang.com/articles/9876
|
请发表评论