前言

在 Go 中,数组与切片是不同的概念,需要加以区别。

数组 array

在 Go 中,数组是值类型,传参时发生的是值拷贝。
因此,若数组作为参数传递,就要根据实际情况考虑到底是传数组本身还是传数组指针。

  1. func try(nums [2]int) {
  2. nums[0] = 100
  3. }
  4. func main() {
  5. nums := [2]int{1, 2}
  6. try(nums)
  7. fmt.Println(nums)
  8. }
  9. 输出如下:
  10. [1 2]

因为修改的数组其实是 nums 的拷贝,所以并不会影响 nums。

切片 slice

创建切片

  1. // 1 直接定义
  2. var s []int = {1, 2, 3} // 函数外
  3. // 2
  4. s := []int{1, 2, 3} // 函数内
  5. // 3
  6. s := make([]int, 0, 3) // make([]T, len int, cap int
  7. s[0] = 1 // 报错,因为底层数组长度为 0 ,表示没有元素,怎么能访问呢

切片本质

切片本质是对数组的引用。

  1. type struct {
  2. ptr *[]int // 指向底层数组的指针
  3. len int // 底层数组的长度
  4. cap int // 底层数组的容量
  5. }
  6. s := make([]int, 2, 4) // 创建一个切片,它指向一个长度为 2,容量为 4 的数组

image.png
当通过 append() 方法往切片中添加元素时,其实是往切片指向的底层数组中添加,有两种情况:

  1. 当底层数组容量足够时,添加需要 O(1) 时间。
  2. 当底层数组容量不够时,添加需要 O(n) 时间。因为会开辟新的内存块,存放原来的元素和新添加的元素。

image.png
cap 的大小由具体的内存分配策略决定。
因此当往切片中添加元素时,如果能知道 cap 而避免发生内存拷贝,性能会比较好。
注意:如果 s := make([]int, 0, 4) 的话
给 s[0-3] 赋值都会出错。

切片传参陷阱

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func insert(nums []int, val int) {
  6. nums = append(nums, val)
  7. }
  8. func main() {
  9. nums := []int{1, 2}
  10. for i := 0; i < 5; i++ {
  11. insert(nums, i)
  12. }
  13. fmt.Println(nums)
  14. }

输出是什么呢?
[1, 2, 0, 1, 2, 3, 4] 吗?
不是!是 [1, 2] ,为什么?
因为传参时,传的是切片的拷贝,即拷贝了 *ptr, len, cap 这三个值的结构体。
调用 append 方法时由于容量不足而发生了扩容(内存复制),两个函数中的 nums 指向了不同的底层数组。
改正方法有 2 种:

  1. 传切片指针 nums *[]int

    1. func insert(nums *[]int, val int) {
    2. *nums = append(*nums, val)
    3. }
  2. 设置返回值,返回新的切片。

    1. func insert(nums []int, val int) []int {
    2. nums = append(nums, val)
    3. return nums
    4. }

切片的切片

在切片上再创建切片,会出现内存陷阱,影响性能。

  1. func copy1(nums []int) []int {
  2. return nums[len(nums) - 3 : ]
  3. }
  4. func copy2(nums []int) []int {
  5. temp := make([]int, 2)
  6. copy(temp, nums[len(nums) - 3 : ])
  7. return temp
  8. }

copy1 和 copy2 都是复制 nums 的最后 2 个元素,但是有如下差别:
copy1 在切片的基础上再切片,新切片和旧切片指向同一个底层数组,底层数组得不到释放。如果底层数组很大,那么很占内存。
copy2 通过内存复制使得新切片指向一个新的底层数组,原底层数组得到释放。