目录

正文

sliceslice[]TTslicesliceslice

数组和切片的区别

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

数组的定义

go 中定义数组的方式有如下两种:

  • 指定长度:
arr := [3]int{1, 2, 3}
  • 不指定长度,由编译器推导出数组的长度:
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

slicesliceslicesliceslice

A1~7sliceBABABABAABABBA

slice 的内存布局

现在假设我们有如下代码:

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

对应的内存布局如下:

说明:

slicelencapslicearraylen30~2cap

切片容量存在的意义

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)
   }
}
sliceslice

最终我们发现,在给切片提前设置容量的情况下,会有一定的性能提升。

切片常用操作

创建切片

我们可以从数组或切片生成新的切片:

end
target[start:end]

说明:

targetstartend

如:

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]
starttarget
end
arr := [3]int{1, 2, 3}
fmt.Println(arr[1:]) // [2, 3]
endstarttarget

除此之外,我们还可以指定新的切片的容量,通过如下这种方式:

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}...) // 追加一个切片

切片复制

copy
copy(dst, src []int)
dstsrccopysrcdst

示例:

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]
dstsrcsrc

从切片删除元素

虽然我们往切片追加元素的操作挺方便的,但是要从切片删除元素就相对麻烦一些了。go 语言本身没有提供从切片删除元素的方法。 如果我们要删除切片中的元素,只有构建出一个新的切片:

对应代码:

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]
a3、4、52~435
3、4、5

切片的容量到底是多少?

假设我们有如下代码:

var a = make([]int, 7, 7)
for i := 0; i &lt; 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

s1arrays256s1s25
s1s2s[start:end]start

切片可以共享底层数组

s1s2s10~2s24~5
s1s2a
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)
s1as1a

切片扩容不会影响原切片

上一小节我们说了,切片可以共享底层数组。但是如果切片扩容的话,那就是一个全新的切片了

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)
a3ba

下面的例子就不一样了:

// 长度为 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)
a3ab := append(a, 4)ba

所以,我们需要尤其注意代码中作为切片的函数参数,如果我们希望在被调函数中修改了切片之后,在 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(&amp;b)
   // [1 2 3 4] 4 6
   fmt.Println(b, len(b), cap(b))
}
test1test1TestSliceatest2test2TestSliceb

总结

[2]int{1, 2}[...]int{1, 2}0 ~ len(x)-1xslicearraylencaparray[1:3]slice[1:3]appendcopyslice[start:end]cap(slice) - startstart
您可能感兴趣的文章: