前言
在 Go 中,数组与切片是不同的概念,需要加以区别。
数组 array
在 Go 中,数组是值类型,传参时发生的是值拷贝。
因此,若数组作为参数传递,就要根据实际情况考虑到底是传数组本身还是传数组指针。
func try(nums [2]int) {
nums[0] = 100
}
func main() {
nums := [2]int{1, 2}
try(nums)
fmt.Println(nums)
}
输出如下:
[1 2]
因为修改的数组其实是 nums 的拷贝,所以并不会影响 nums。
切片 slice
创建切片
// 1 直接定义
var s []int = {1, 2, 3} // 函数外
// 2
s := []int{1, 2, 3} // 函数内
// 3
s := make([]int, 0, 3) // make([]T, len int, cap int
s[0] = 1 // 报错,因为底层数组长度为 0 ,表示没有元素,怎么能访问呢
切片本质
切片本质是对数组的引用。
type struct {
ptr *[]int // 指向底层数组的指针
len int // 底层数组的长度
cap int // 底层数组的容量
}
s := make([]int, 2, 4) // 创建一个切片,它指向一个长度为 2,容量为 4 的数组
当通过 append() 方法往切片中添加元素时,其实是往切片指向的底层数组中添加,有两种情况:
- 当底层数组容量足够时,添加需要 O(1) 时间。
- 当底层数组容量不够时,添加需要 O(n) 时间。因为会开辟新的内存块,存放原来的元素和新添加的元素。
cap 的大小由具体的内存分配策略决定。
因此当往切片中添加元素时,如果能知道 cap 而避免发生内存拷贝,性能会比较好。
注意:如果 s := make([]int, 0, 4) 的话
给 s[0-3] 赋值都会出错。
切片传参陷阱
package main
import (
"fmt"
)
func insert(nums []int, val int) {
nums = append(nums, val)
}
func main() {
nums := []int{1, 2}
for i := 0; i < 5; i++ {
insert(nums, i)
}
fmt.Println(nums)
}
输出是什么呢?
是 [1, 2, 0, 1, 2, 3, 4]
吗?
不是!是 [1, 2]
,为什么?
因为传参时,传的是切片的拷贝,即拷贝了 *ptr, len, cap 这三个值的结构体。
调用 append 方法时由于容量不足而发生了扩容(内存复制),两个函数中的 nums 指向了不同的底层数组。
改正方法有 2 种:
传切片指针
nums *[]int
。func insert(nums *[]int, val int) {
*nums = append(*nums, val)
}
设置返回值,返回新的切片。
func insert(nums []int, val int) []int {
nums = append(nums, val)
return nums
}
切片的切片
在切片上再创建切片,会出现内存陷阱,影响性能。
func copy1(nums []int) []int {
return nums[len(nums) - 3 : ]
}
func copy2(nums []int) []int {
temp := make([]int, 2)
copy(temp, nums[len(nums) - 3 : ])
return temp
}
copy1 和 copy2 都是复制 nums 的最后 2 个元素,但是有如下差别:
copy1 在切片的基础上再切片,新切片和旧切片指向同一个底层数组,底层数组得不到释放。如果底层数组很大,那么很占内存。
copy2 通过内存复制使得新切片指向一个新的底层数组,原底层数组得到释放。