slice(切片)是 go 里边十分常用的一种数据结构,它代表了一个变长的序列,序列中的每个元素都有相同的数据类型。 一个 slice 类型一般写作 []T,其间 T 代表 slice 中元素的类型;slice 的语法和数组很像,可是 slice 没有固定长度

数组和切片的差异

数组有确定的长度,而切片的长度不固定,并且能够自动扩容。

数组的界说

go 中界说数组的方法有如下两种:

  1. 指定长度:
arr := [3]int{1, 2, 3}
  1. 不指定长度,由编译器推导出数组的长度:
arr := [...]{1, 2, 3}

上面这两种界说方法都界说了一个长度为 3 的数组。正如咱们所见,长度是数组的一部分,界说数组的时分长度已经确定下来了

切片的界说

切片的界说方法跟数组很像,只不过界说切片的时分不必指定长度

s := []int{1, 2, 3}

在上面界说切片的代码中,咱们能够看到其实跟数组仅有的差异便是少了个长度。 那其实咱们能够把切片看作是一个无限长度的数组。 当然,实际上它并不是无限的,它只是在切片包容不下新的元素的时分,会自动进行扩容,然后能够包容更多的元素。

数组和切片的类似之处

正如咱们上面看到的那样,数组和切片两者其实十分类似,在实际运用中,它们也是有些类似的。

比如,经过下标来拜访元素:

arr := [3]int{1, 2, 3}
// 经过下标拜访
fmt.Println(arr[1]) // 2
s := []int{1, 2, 3}
// 经过下标拜访
fmt.Println(s[1]) // 2

数组的限制

咱们知道了,数组的长度是固定的,这也就意味着假定咱们想往数组里边增加一个元素会比较费事, 咱们需求新建一个更大的数组,然后将旧的数据仿制曩昔,然后将新的元素写进去,如:

// 往数组 arr 增加一个元素:4
arr := [3]int{1, 2, 3}
// 新建一个更大容量的数组
var arr1 [4]int
// 仿制旧数组的数据
for i := 0; i < len(arr); i++ {
    arr1[i] = arr[i]
}
// 参加新的元素:4
arr1[3] = 4
fmt.Println(arr1)

这样一来就十分的繁琐,假定咱们运用切片,就能够省去这些步骤:

// 界说一个长度为 3 的数组
arr := [3]int{1, 2, 3}
// 从数组创立一个切片
s := arr[:]
// 增加一个元素
s = append(s, 4)
fmt.Println(s)

因为数组固定长度的缺点,实际运用中切片会运用得更加普遍。

从头了解 slice

在开端之前,咱们来看看 slice 这个单词的意思:作为名词,slice 的意思有 片;部分;(切下的食物)薄片;,作为动词,slice 的意思有 切;把…切成(薄)片; 的意思。 从这个视点动身,咱们能够把 slice 了解为从某个数组上 切下来的一部分(从这个视点看,slice 这个命名十分的形象)。咱们能够看看下图:

go slice 基本用法

在这个图中,A 是一个保存了数字 1~7sliceB 是从 A切下来的一部分,而 B 只包括了 A 中的一部分数据。 咱们能够把 B 了解为 A 的一个 视图B 中的数据是 A 中的数据的一个 引证,而不是 A 中数据的一个 仿制 (也便是说,咱们修改 B 的时分,A 中的数据也会被修改,当然会有破例,那便是 B 产生扩容的时分,再去修改 B 的话就影响不了 A 了)。

slice 的内存布局

现在假定咱们有如下代码:

// 创立一个切片,长度为 3,容量为 7
var s = make([]int, 3, 7)
s[0] = 1
s[1] = 2
s[2] = 3
fmt.Println(s)

对应的内存布局如下:

go slice 基本用法

说明:

  • slice 底层其实也是数组,可是除了数组之外,还有两个字段记载切片的长度和容量,分别是 lencap
  • 上图中,slice 中的 array 便是切片的底层数组,因为它的长度不是固定的,所以运用了指针来保存,指向了别的一片内存区域。
  • len 表明晰切片的长度,切片的长度也便是咱们能够操作的下标,上面的切片长度为 3,这也就意味着咱们切片能够操作的下标规模是 0~2。超出这个规模的下标会报错。
  • cap 表明晰切片的容量,也便是切片扩容之前能够包容的元素个数

切片容量存在的含义

对于咱们日常开发来说,slice 的容量其实大多数时分不是咱们需求重视的点,并且因为容量的存在,也给开发者带来了必定的困惑。 那么容量存在的含义是什么呢?含义就在于避免内存的频频分配带来的功能下降(容量也便是提早分配的内存大小)。

比如,假定咱们有一个切片,然后咱们知道需求往它里边寄存 1w 个元素, 假定咱们不指定容量的话,那么切片就会在它寄存不下新的元素的时分进行扩容, 这样一来,可能在咱们寄存这 1w 个元素的时分需求进行屡次扩容, 这也就意味着需求进行屡次的内存分配。这样就会影响运用的功能。

咱们能够经过下面的比如来简略了解一下:

// Benchmark1-20       100000000          11.68 ns/op
func Benchmark1(b *testing.B) {
   var s []int
   for i := 0; i < b.N; i++ {
      s = append(s, 1)
   }
}
// Benchmark2-20       134283985           7.482 ns/op
func Benchmark2(b *testing.B) {
   var s []int = make([]int, 10, 100000000)
   for i := 0; i < b.N; i++ {
      s = append(s, 1)
   }
}

在第一个比如中,没有给 slice 设置容量,这样它就只会在切片包容不下新元素的时分才会进行扩容,这样就会需求进行屡次扩容。 而第二个比如中,咱们先给 slice 设置了一个足够大的容量,那么它就不需求进行频频扩容了。

终究咱们发现,在给切片提早设置容量的情况下,会有必定的功能提高。

切片常用操作

创立切片

咱们能够从数组或切片生成新的切片:

留意:生成的切片不包括 end

target[start:end]

说明:

  • target 表明方针数组或许切片
  • start 对应方针方针的开端索引(包括)
  • end 对应方针方针的完毕索引(不包括)

如:

s := []int{1, 2, 3}
s1 := s[1:2]    // 包括下标 1,不包括下标 2
fmt.Println(s1) // [2]
arr := [3]int{1, 2, 3}
s2 := arr[1:2]
fmt.Println(s2) // [2]

在这种初始化方法中,咱们能够省掉 start

arr := [3]int{1, 2, 3}
fmt.Println(arr[:2]) // [1, 2]

省掉 start 的情况下,便是从 target 的第一个元素开端。

咱们也能够省掉 end

arr := [3]int{1, 2, 3}
fmt.Println(arr[1:]) // [2, 3]

省掉 end 的情况下,便是从 start 索引处的元素开端直到 target 的终究一个元素处。

除此之外,咱们还能够指定新的切片的容量,经过如下这种方法:

target[start:end:cap]

比如:

arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s := arr[1:4:5]
fmt.Println(s, len(s), cap(s)) // [2 3 4] 3 4

往切片中增加元素

咱们前面说过了,假定咱们想往数组里边增加元素,那么咱们有必要开辟新的内存,将旧的数组仿制曩昔,然后才能将新的元素参加进去。

可是切片就相对简略,咱们能够运用 append 这个内置函数交游切片中参加新的元素:

var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素
a = append(a, []int{1,2,3}...) // 追加一个切片

切片仿制

go 有一个内置函数 copy 能够将一个切片的内容仿制到别的一个切片中:

copy(dst, src []int)

第一个参数 dst 是方针切片,第二个参数 src 是源切片,调用 copy 的时分会把 src 的内容仿制到 dst 中。

示例:

var a []int
var b []int = []int{1, 2, 3}
// a 的容量为 0,包容不下任何元素
copy(a, b)
fmt.Println(a) // []
a = make([]int, 3, 3) // 给 a 分配内存
copy(a, b)
fmt.Println(a) // [1 2 3]

需求留意的是,假定 dst 的长度比 src 的长度小,那么只会截取 src 的前面一部分。

从切片删去元素

尽管咱们往切片追加元素的操作挺方便的,可是要从切片删去元素就相对费事一些了。go 语言自身没有提供从切片删去元素的方法。 假定咱们要删去切片中的元素,只有构建出一个新的切片:

go slice 基本用法

对应代码:

var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
    a[i] = i + 1
}
fmt.Println(a) // [1 2 3 4 5 6 7]
var b []int
b = append(b, a[:2]...) // [1 2]
b = append(b, a[5:]...) // [1 2 6 7]
fmt.Println(b) // [1 2 6 7]

在这个比如中,咱们想从 a 中删去 3、4、5 这三个元素,也便是下标 2~4 的元素, 咱们的做法是,新建了一个新的切片,然后将 3 前面的元素参加到这个新的切片中, 再将 5 后边的元素参加到这个新切片中。

终究得到的切片便是删去了 3、4、5 三个元素之后的切片了。

切片的容量究竟是多少?

假定咱们有如下代码:

var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
    a[i] = i + 1
}
// [1 2 3 4 5 6 7]
fmt.Println(a)
s1 := a[:3]
// [1 2 3] 3 7
fmt.Println(s1, len(s1), cap(s1))
s2 := a[4:6]
// [5 6] 2 3
fmt.Println(s2, len(s2), cap(s2))

s1s2 能够用下图表明:

go slice 基本用法

  • s1 只能拜访 array 的前三个元素,s2 只能拜访 56 这两个元素。
  • s1 的容量是 7(底层数组的长度)
  • s2 的容量是 3,从 5 地点的索引处直究竟层数组的结尾。

对于 s1s2,咱们都没有指定它的容量,可是咱们打印发现它们都有容量, 其实在切片中,咱们从切片中生成一个新的切片的时分,假定咱们不指定容量, 那新切片的容量便是 s[start:end] 中的 start 直究竟层数组的终究一个元素的长度。

切片能够同享底层数组

切片最需求留意的点是,当咱们从一个切片中创立新的切片的时分,两者会同享同一个底层数组, 如上图的那样,s1s2 都引证了同一个底层的数组不同的索引, s1 引证了底层数组的 0~2 下标规模,s2 引证了底层数组 4~5 下标规模。

这意味着,当咱们修改 s1s2 的时分,本来的切片 a 也会产生改动:

var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
    a[i] = i + 1
}
// [1 2 3 4 5 6 7]
fmt.Println(a)
s1 := a[:3]
// [1 2 3]
fmt.Println(s1)
s1[1] = 100
// [1 100 3 4 5 6 7]
fmt.Println(a)
// [1 100 3]
fmt.Println(s1)

在上面的比如中,s1 这个切片引证了和 a 相同的底层数组, 然后在咱们修改 s1 的时分,a 也产生了改动。

切片扩容不会影响原切片

上一小节咱们说了,切片能够同享底层数组。可是假定切片扩容的话,那便是一个全新的切片了

var a = []int{1, 2, 3}
// [1 2 3] 3 3
fmt.Println(a, len(a), cap(a))
// a 包容不下新的元素了,会进行扩容
b := append(a, 4)
// [1 2 3 4] 4 6
fmt.Println(b, len(b), cap(b))
b[1] = 100
// [1 2 3]
fmt.Println(a)
// [1 100 3 4]
fmt.Println(b)

在上面这个比如中,a 是一个长度和容量都是 3 的切片,这也就意味着,这个切片已经满了。 在这种情况下,咱们再往其间追加元素的时分,就会进行扩容,生成一个新的切片。 因而,咱们能够看到,咱们修改了 b 的时分,并没有影响到 a

下面的比如就不相同了:

// 长度为 2,容量为 3
var a = make([]int, 2, 3)
a[0] = 1
a[1] = 2
// [1 2] 2 3
fmt.Println(a, len(a), cap(a))
// a 还能够包容新的元素,不必扩容
b := append(a, 4)
// [1 2 4] 3 3
fmt.Println(b, len(b), cap(b))
b[1] = 100
// [1 100]
fmt.Println(a)
// [1 100 4]
fmt.Println(b)

在后边这个比如中,咱们只是简略地改了一下 a 初始化的方法,改成了只放入两个元素,可是容量仍是 3, 在这种情况下,a 能够再包容一个元素,这样在 b := append(a, 4) 的时分,创立的 b 底层的数组其实跟 a 的底层数组依然是相同的。

所以,咱们需求特别留意代码中作为切片的函数参数,假定咱们希望在被调函数中修改了切片之后,在 caller 里边也能看到效果的话,最好是传递指针

func test1(s []int) {
   s = append(s, 4)
}
func test2(s *[]int) {
   *s = append(*s, 4)
}
func TestSlice(t *testing.T) {
   var a = []int{1, 2, 3}
   // [1 2 3] 3 3
   fmt.Println(a, len(a), cap(a))
   test1(a)
   // [1 2 3] 3 3
   fmt.Println(a, len(a), cap(a))
   var b = []int{1, 2, 3}
   // [1 2 3] 3 3
   fmt.Println(b, len(b), cap(b))
   test2(&b)
   // [1 2 3 4] 4 6
   fmt.Println(b, len(b), cap(b))
}

在上面的比如中,test1 接纳的是值参数,所以在 test1 中切片产生扩容的时分,TestSlice 里边的 a 仍是没有产生改动。 而 test2 接纳的是指针参数,所以在 test2 中产生切片扩容的时分,TestSlice 里边的 b 也产生了改动。

总结

  • 数组跟切片的运用上有点类似,可是数组代表的是有固定长度的数据序列,而切片代表的是没有固定长度的数据序列。
  • 数组的长度是类型的一部分,有两种界说数组的方法:[2]int{1, 2}[...]int{1, 2}
  • 数组跟切片都能够经过下标来拜访其间的元素,能够拜访的下标规模都是 0 ~ len(x)-1x 表明的是数组或许切片。
  • 数组无法追加新的元素,切片能够追加任意数量的元素。
  • slice 的数据结构里边包括了:array 底层数组指针、len 切片长度、cap 切片容量。
  • 创立切片的时分,指定一个合适的容量能够减少内存分配的次数,然后在必定程度上提高程序功能。
  • 咱们能够从数组或许切片创立一个新的切片:array[1:3] 或许 slice[1:3]
  • 运用 append 内置函数能够往切片中增加新的元素。
  • 运用 copy 内置函数能够将一个切片的内容仿制到别的一个切片中。
  • 切片删去元素没有好的办法,只能截取被删去元素前后的数据,然后仿制到一个新的切片中。
  • 假定咱们经过 slice[start:end] 的方法从切片中创立一个新的切片,那么这个新的切片的容量是 cap(slice) - start,也便是,从 start 究竟层数组终究一个元素的长度。
  • 运用切片的时分需求留意:切片之间会同享底层数组,其间一个切片修改了切片的元素的时分,也会反映到其他切片上。
  • 函数调用的时分,假定被调函数内产生扩容,调用者是无法知道的。假定咱们不想错过在被调函数内切片的改变,咱们能够传递指针作为参数。