本文详细介绍Golang的切片与数组,包括他们的联系,区别,底层实现和使用注意事项等。
文章目录
数组与切片的异同
相同之处
- 集合类型:数组和切片均属于集合类类型,其值均可用于存储某一种类型的元素。
- 内存布局:在内存中,数组和切片的元素存储是连续分配的。
- 访问方式:两者都可以通过下标来访问单个元素。
区别
-
数组:
- 数组的长度是固定的,必须在声明时指定,且之后无法改变。
- 数组的长度是其类型的一部分,例如
[3]int
和[4]int
是不同的类型。 - 由于长度固定,数组在实际开发中使用较少。
-
切片:
- 切片更加灵活,是数组的封装和增强。
- 切片的长度可变,其类型字面量中只有元素类型,没有长度(可通过
make
函数初始化时指定长度和容量)。 - 切片的长度可随着添加元素而动态增长,但不会因移除元素而减少(直到没有引用时垃圾回收机制才会释放)。
切片(Slice)源码解析
切片的长度和容量均可动态扩展,其底层结构如下:
type slice struct {
array unsafe.Pointer // 指向切片中第一个元素的地址
len int // 切片长度
cap int // 切片容量
}
array
:指向底层数组的内存地址。len
(长度):返回集合中的元素数量。切片中实际包含的元素数量,必须小于等于cap
。cap
(容量):返回切片的最大长度(当重新切片时可达到的长度)。从切片第一个元素到底层数组末尾元素的最大可用空间。
Go 源码中 len()
和 cap()
定义
len()
示例定义:
// The len built-in function returns the length of v, according to its type:
// Array: the number of elements in v.
// Pointer to array: the number of elements in *v (even if v is nil).
// Slice, or map: the number of elements in v; if v is nil, len(v) is zero.
// String: the number of bytes in v.
// Channel: the number of elements queued (unread) in the channel buffer;
// if v is nil, len(v) is zero.
// For some arguments, such as a string literal or a simple array expression, the
// result can be a constant. See the Go language specification's "Length and
// capacity" section for details.
func len(v Type) int
描述:
- 数组:返回数组中元素数量。
- 指针数组:返回指向数组的元素数量。
- 切片或map:返回元素数量(若切片为
nil
,长度为 0)。 - 字符串:返回字节长度。
- 通道:返回缓冲区中未读取的元素数量(若通道为
nil
,长度为 0)。
cap()
示例定义:
// The cap built-in function returns the capacity of v, according to its type:
// Array: the number of elements in v (same as len(v)).
// Pointer to array: the number of elements in *v (same as len(v)).
// Slice: the maximum length the slice can reach when resliced;
// if v is nil, cap(v) is zero.
// Channel: the channel buffer capacity, in units of elements;
// if v is nil, cap(v) is zero.
// For some arguments, such as a simple array expression, the result can be a
// constant. See the Go language specification's "Length and capacity" section for
// details.
func cap(v Type) int
描述:
- 数组:返回元素数量(与
len()
一致)。 - 指针数组:返回指向数组的元素数量。
- 切片:返回可达的最大长度(若切片为
nil
,容量为 0)。 - 通道:返回缓冲区的最大容量。
注意:map没有cap()。
长度与容量示例
func main() {
a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(len(a), cap(a)) // 输出: 10 10
b := a[3:4]
fmt.Println(len(b), cap(b)) // 输出: 1 7
}
解释:
len(b)
为切片长度,即4-3=1
。cap(b)
为切片容量,即从索引 3 开始,直到底层数组末尾的元素数,即10-3=7
。
append()
函数
-
append
函数的原型:func append(slice []Type, elems ...Type) []Type
- 支持可变参数,可追加多个值。
- 可使用
...
直接追加另一个 slice。
-
调用规则:
append
函数返回新的 slice,必须使用返回值,未使用返回值的调用会编译失败。一般会使用原变量赋值。
-
append()
用于向切片添加元素或合并两个切片,行为如下:- 容量足够:
- 直接在原底层数组后追加元素,返回的切片与原切片共享底层数组。
- 此时,原切片的值会发生变化。
- 容量不足:
- 创建一个新的底层数组,将原切片的数据复制到新数组,再追加元素。
- 此时,原切片不会变化。
- 容量足够:
面试考点:
- 如果多个切片共享一个底层数组,
append
超出容量时,切片迁移到新内存,其他切片仍指向旧底层数组。 - 示例:
s := []int{1, 2, 3} x := append(s, 4) // 底层数组有剩余空间,不迁移 y := append(s, 5) // 底层数组已满,迁移到新内存 fmt.Println(s, x, y) // 输出: [1 2 3] [1 2 3 4] [1 2 3 5]
Go 切片扩容机制
基本原理
- 使用
append()
向切片追加元素时,如果底层数组容量不足,切片会迁移到新的内存位置。 - 扩容过程:
- 在底层数组追加元素。
- 若容量不足,创建新数组并迁移原数据。
- 新切片预留一定容量 buffer,以降低未来迁移成本。
那么这个buffer会预留多少呢?
扩容策略(依据 Go 版本)
-
Go 1.18 之前:
- 新容量 > 旧容量的 2 倍:直接将新容量作为扩容后的容量。
- 旧容量 < 1024:扩容后容量为旧容量的 2 倍。
- 旧容量 ≥ 1024:每次增加旧容量的 1/4,直到满足
newcap >= cap
。 - 溢出检查:若容量cap计算值溢出,最终容量直接设置为新申请容量。
-
Go 1.18 及之后:
- 原容量 < 256:新容量为原容量的 2 倍。
- 原容量 ≥ 256:
newcap = oldcap + (oldcap + 3*256)/4
- 内存对齐:最终容量经过内存对齐处理(如 8 字节倍数),可能略大于计算值。例如:
s := make([]int, 2, 2) s = append(s, 4, 5, 6) fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // 输出: len=5, cap=6
扩容源码解析
- 函数调用:
append
会调用growslice
完成扩容。 - 内存分配:
- 计算新容量
newcap
。 - 调用
roundupsize
函数完成内存对齐。 - 分配新内存,将旧数据复制到新数组,追加新元素。
- 计算新容量
- 扩容后的特性:
- 长度(
len
):仅增加到实际元素数量。 - 容量(
cap
):扩容后值变大,满足未来可能的append
操作。
- 长度(
常见误区
-
未使用
append
返回值:- 忘记更新切片引用会导致数据未正确扩容。
s := []int{1, 2} append(s, 3) // 错误,未保存返回值 fmt.Println(s) // 输出: [1 2]
-
忽略内存对齐影响:
- 假设容量完全等于理论计算值,未考虑内存对齐可能导致实际值略大。
-
未意识到
append
返回新切片:- 原切片数据可能未被更新。
建议
为了避免意外修改原切片数据,可以通过切片的第三个索引限制容量,从而强制触发新底层数组的创建。例如:
a := []int{1, 2, 3, 4}
b := a[:2:2] // 限制长度和容量相等
b = append(b, 5) // 生成新的切片,原切片 a 不受影响
切片作为函数参数传递
-
多个切片共享底层数组,所以作为函数参数传递需要特别注意:不同的切片可能同时指向同一个底层数组,因此对其中一个切片的操作可能影响到其他切片。
-
作为函数参数:
- 切片本质是一个结构体。当切片作为函数参数传递时,切片本身是按值传递的:
- 直接传切片:按值传递切片结构体
当切片作为函数参数传递时,传递的是切片结构体的值(包括指向底层数组的指针)。- 对切片本身(结构体字段如
len
、cap
)的修改不会影响调用者的切片,因为传递的是结构体的副本。 - 对切片底层数组的修改会影响调用者的切片,因为底层数组是共享的。
- 对切片本身(结构体字段如
- 传递切片指针:按值传递切片结构体的指针
当切片的指针传递到函数时,函数可以直接修改调用者的切片结构体本身(例如修改len
和cap
),并且仍然可以修改底层数组。
- 直接传切片:按值传递切片结构体
- 函数参数无论是直接传递切片还是传递切片的指针,底层数组的值都可能会被改变,因为底层数组是通过指针访问的。是否改变底层数组,应该看容量是否足够,和append() 函数与原数组变化问题一样。
示例:
func modifySlice(s []int) { s[0] = 42 // 修改底层数组 } func main() { nums := []int{1, 2, 3} modifySlice(nums) fmt.Println(nums) // 输出: [42, 2, 3] }
- 切片本质是一个结构体。当切片作为函数参数传递时,切片本身是按值传递的:
-
Go 的参数传递:
- Go 语言中只有值传递,没有引用传递。即使传递切片,也是将切片的结构体副本传入。
- 通过切片的
array
字段(底层数组指针),可以操作底层数组的值,从而间接修改原始数据。