前言:理解了 len 和 cap,然后呢?
上一篇学习 Go Array 和 Slice 时,我通过几组实验理解了:
Array 固定长度
Slice 可以动态扩展
Slice 有 len 和 cap
多个 Slice 可能共享底层数组
append 可能复用原来的底层数组
容量不足时才会申请新的存储空间
其中最让我意外的,是下面这段代码:
a := []int{10, 20, 30, 40, 50}
b := a[1:3]
b = append(b, 999)
最终可能得到:
a:[10 20 30 999 50]
b:[20 30 999]
我明明只操作了 b,为什么 a 也发生了变化?
原因是 a 和 b 仍然共享同一个底层数组,而且 b 还有剩余容量,所以 append 直接复用了原来的空间。
理解到这里以后,新的问题自然就出现了:
make() 是做什么的?
为什么可以提前设置 Slice 的容量?
copy() 是否会复制出独立数据?
如果明确不想共享底层数组,应该怎么做?
Go 的 Slice 又该怎么删除元素?
这一篇,就继续通过实际代码把这些问题解决掉。
一、使用 make 创建 Slice
前面创建 Slice,主要使用的是字面量:
numbers := []int{10, 20, 30}
Go 还提供了另外一种非常常见的创建方式:
numbers := make([]int, 3)
完整代码:
package main
import "fmt"
func main() {
numbers := make([]int, 3)
fmt.Println("numbers:", numbers)
fmt.Println("len:", len(numbers))
fmt.Println("cap:", cap(numbers))
}
运行结果:
numbers: [0 0 0]
len: 3
cap: 3
make([]int, 3) 的结构是:
make([]元素类型, 长度)
所以:
make([]int, 3)
表示创建一个长度为 3 的 int Slice。
因为没有手动设置元素,所以三个位置都使用 int 的零值:
0
这三个元素已经真实存在,可以直接通过下标修改:
numbers[0] = 10
numbers[1] = 20
numbers[2] = 30
但下面这段代码仍然会越界:
numbers[3] = 40
因为当前:
len(numbers) = 3
合法下标只有:
0、1、2
从前端视角看:它和 new Array(3) 并不完全一样
JavaScript 中可以这样创建数组:
const numbers = new Array(3);
console.log(numbers);
// [empty × 3]
虽然:
numbers.length === 3
但这里的三个位置是空槽。
Go:
numbers := make([]int, 3)
得到的是:
[0 0 0]
三个元素已经被初始化成 int 的零值。
目前可以这样对比:
| Go | JavaScript |
|---|---|
make([]int, 3) | new Array(3) |
| 长度都是 3 | 长度都是 3 |
三个元素已经是 0 | 三个位置是空槽 |
读取元素得到 0 | 读取元素通常得到 undefined |
元素类型固定为 int | 元素类型不固定 |
所以两者看起来相似,但初始化行为并不相同。
二、同时设置长度和容量
make 创建 Slice 时,还可以传入第三个参数:
numbers := make([]int, 0, 5)
完整结构是:
make([]元素类型, 长度, 容量)
所以:
make([]int, 0, 5)
表示:
当前长度:0
提前准备的容量:5
运行下面的代码:
package main
import "fmt"
func main() {
numbers := make([]int, 0, 5)
fmt.Println("创建后:", numbers)
fmt.Println("len:", len(numbers))
fmt.Println("cap:", cap(numbers))
numbers = append(numbers, 10)
fmt.Println("追加 10 后:", numbers)
fmt.Println("len:", len(numbers))
fmt.Println("cap:", cap(numbers))
numbers = append(numbers, 20, 30)
fmt.Println("继续追加后:", numbers)
fmt.Println("len:", len(numbers))
fmt.Println("cap:", cap(numbers))
}
运行结果:
创建后: []
len: 0
cap: 5
追加 10 后: [10]
len: 1
cap: 5
继续追加后: [10 20 30]
len: 3
cap: 5
这里能够明显看到:
len:0 → 1 → 3
cap:5 → 5 → 5
随着元素不断添加,len 会发生变化。
但因为提前准备的容量还没有用完,所以 cap 一直保持为 5。
有容量,不代表可以直接使用下标
下面这段代码会发生越界:
numbers := make([]int, 0, 5)
numbers[0] = 10
虽然:
cap = 5
但是:
len = 0
能否通过下标访问元素,判断的是 len,不是 cap。
所以此时必须使用:
numbers = append(numbers, 10)
可以先这样理解:
len:当前真正存在并且可以访问的元素数量
cap:从当前起点开始,底层已经准备好的空间
有空间,不代表这些元素已经进入当前 Slice。
从前端视角看:它也不是 new Array(5)
JavaScript:
const numbers = new Array(5);
创建以后:
numbers.length === 5;
Go:
numbers := make([]int, 0, 5)
创建以后:
len = 0
cap = 5
它在使用体验上更接近:
const numbers = [];
只是 Go 在底层提前准备了可以容纳若干元素的空间。
JavaScript 业务代码通常只关注:
numbers.length
底层数组什么时候扩容、准备了多少空间,通常由 JavaScript 引擎处理。
Go 则通过:
len(numbers)
cap(numbers)
把这两个概念直接暴露给了开发者。
三、容量用完以后会发生什么?
接下来从容量为 5 的空 Slice 开始,连续追加六个元素:
package main
import "fmt"
func main() {
numbers := make([]int, 0, 5)
for i := 1; i <= 6; i++ {
numbers = append(numbers, i*10)
fmt.Printf(
"numbers=%v len=%d cap=%d\n",
numbers,
len(numbers),
cap(numbers),
)
}
}
我的运行结果是:
numbers=[10] len=1 cap=5
numbers=[10 20] len=2 cap=5
numbers=[10 20 30] len=3 cap=5
numbers=[10 20 30 40] len=4 cap=5
numbers=[10 20 30 40 50] len=5 cap=5
numbers=[10 20 30 40 50 60] len=6 cap=10
前五次 append 时,原来的空间还够,所以:
cap 一直是 5
追加第六个元素时:
len = 5
cap = 5
原来的空间已经用满。
Go 需要准备更大的底层存储空间,所以这次实验中:
cap:5 → 10
不过这里不能死记:
容量每次一定翻倍
Slice 的容量增长策略属于 Go 运行时的实现细节,业务代码不应该依赖某个固定倍数。
真正需要理解的是:
len < cap
→ append 可能继续复用当前底层数组
len == cap 后继续 append
→ 需要准备更大的底层空间
四、为什么要提前设置容量?
假设已经知道接下来大约会添加 1000 条数据,可以这样创建 Slice:
numbers := make([]int, 0, 1000)
这样 Go 可以提前准备一部分空间。
如果写成:
numbers := []int{}
然后连续 append 1000 次,过程中可能多次发生:
容量不足
→ 申请更大的底层数组
→ 复制原来的元素
→ 继续追加
提前设置一个合理的容量,可以减少扩容和数据复制。
不过下面这两种写法完全不同:
a := make([]int, 1000)
b := make([]int, 0, 1000)
第一种:
a := make([]int, 1000)
表示 Slice 中已经存在 1000 个零值元素。
第二种:
b := make([]int, 0, 1000)
表示当前没有元素,只是提前准备了 1000 的容量。
如果后续准备使用 append 添加数据,通常更符合需求的是:
b := make([]int, 0, 1000)
否则:
a := make([]int, 1000)
a = append(a, 10)
10 会成为第 1001 个元素,前面已经存在 1000 个 0。
这是前端开发刚接触 make 时非常容易踩的坑。
五、直接赋值并不等于复制数据
接下来解决另外一个问题:
怎样得到一份真正独立的 Slice?
先看下面的代码:
package main
import "fmt"
func main() {
original := []int{10, 20, 30}
copied := original
copied[0] = 999
fmt.Println("original:", original)
fmt.Println("copied:", copied)
}
运行结果:
original: [999 20 30]
copied: [999 20 30]
这一句:
copied := original
并没有复制出一份完全独立的数据。
可以暂时理解为,它复制了 Slice 的描述信息,但两个 Slice 仍然指向同一个底层数组:
original ─┐
├─→ [10 20 30]
copied ───┘
所以:
copied[0] = 999
也会让:
original[0]
变成 999。
从前端角度看,它很像:
const original = [10, 20, 30];
const copied = original;
copied[0] = 999;
console.log(original);
// [999, 20, 30]
两个变量引用的是同一个数组对象。
六、使用 make 和 copy 创建独立数据
如果希望复制出独立的 Slice,可以先创建目标空间:
copied := make([]int, len(original))
然后使用:
copy(copied, original)
完整代码:
package main
import "fmt"
func main() {
original := []int{10, 20, 30}
copied := make([]int, len(original))
count := copy(copied, original)
fmt.Println("复制数量:", count)
fmt.Println("修改前 original:", original)
fmt.Println("修改前 copied:", copied)
copied[0] = 999
fmt.Println("修改后 original:", original)
fmt.Println("修改后 copied:", copied)
}
运行结果:
复制数量: 3
修改前 original: [10 20 30]
修改前 copied: [10 20 30]
修改后 original: [10 20 30]
修改后 copied: [999 20 30]
这次修改 copied 后,original 没有发生变化。
因为:
copied := make([]int, len(original))
先创建了新的底层存储。
然后:
copy(copied, original)
把元素复制了进去。
现在两者的关系变成:
original → [10 20 30]
copied → [999 20 30]
数据一开始相同,但底层数组已经分开。
七、copy 到底会复制多少个元素?
copy 的语法是:
copy(目标Slice, 来源Slice)
例如:
copy(copied, original)
它还会返回实际复制的元素数量:
count := copy(copied, original)
需要特别注意:
实际复制数量取决于目标和来源中较短的一方。
也就是:
复制数量 = min(目标长度, 来源长度)
运行下面的实验:
package main
import "fmt"
func main() {
source := []int{10, 20, 30, 40}
shortTarget := make([]int, 2)
shortCount := copy(shortTarget, source)
fmt.Println("较短目标:", shortTarget)
fmt.Println("复制数量:", shortCount)
longTarget := make([]int, 6)
longCount := copy(longTarget, source)
fmt.Println("较长目标:", longTarget)
fmt.Println("复制数量:", longCount)
emptyTarget := make([]int, 0, 5)
emptyCount := copy(emptyTarget, source)
fmt.Println("只有容量的目标:", emptyTarget)
fmt.Println("len:", len(emptyTarget))
fmt.Println("cap:", cap(emptyTarget))
fmt.Println("复制数量:", emptyCount)
}
运行结果:
较短目标: [10 20]
复制数量: 2
较长目标: [10 20 30 40 0 0]
复制数量: 4
只有容量的目标: []
len: 0
cap: 5
复制数量: 0
最值得注意的是:
emptyTarget := make([]int, 0, 5)
copy(emptyTarget, source)
虽然:
cap = 5
但是:
len = 0
目标 Slice 中没有可以直接访问的位置,所以一个元素都不会被复制。
这再次证明:
是否能够访问和复制元素,取决于 len
不是 cap
因此,复制整个 Slice 时通常写成:
target := make([]int, len(source))
copy(target, source)
八、使用 append 快速复制 Slice
除了 make + copy,还可以这样复制整个 Slice:
copied := append([]int{}, original...)
完整代码:
package main
import "fmt"
func main() {
original := []int{10, 20, 30}
copied := append([]int{}, original...)
copied[0] = 999
fmt.Println("original:", original)
fmt.Println("copied:", copied)
}
运行结果:
original: [10 20 30]
copied: [999 20 30]
这里的:
original...
表示把 original 中的元素展开。
下面这句:
append([]int{}, original...)
可以暂时理解成:
append([]int{}, 10, 20, 30)
前面的:
[]int{}
是一个新的空 Slice。
原来的容量不足以容纳这些元素,所以 append 会准备新的底层存储。
从前端视角看:很像展开运算符
JavaScript:
const original = [10, 20, 30];
const copied = [...original];
copied[0] = 999;
console.log(original);
// [10, 20, 30]
console.log(copied);
// [999, 20, 30]
Go:
original := []int{10, 20, 30}
copied := append([]int{}, original...)
两者在当前数字数组的场景下非常接近。
可以先这样选择:
需要明确控制目标长度、获取复制数量
→ make + copy
只是想快速复制整个 Slice
→ append + ...
不过这里要保留一个提醒:
copy 和 append(..., source...)
复制的是 Slice 中的元素
不代表任何情况下都是深拷贝
当前使用的是:
[]int
元素只是整数,所以复制以后已经独立。
如果以后出现:
[][]int
或者 Slice 中存放的是 Map、指针等类型,内部数据仍然可能共享。
这一点等学到嵌套 Slice、Map 和指针以后再继续验证。
九、Go 的 Slice 怎么删除元素?
Go 的 Slice 没有直接对应的:
remove()
splice()
常见删除方式是把目标元素前后的两段重新拼接。
例如删除索引 2,也就是数字 30:
package main
import "fmt"
func main() {
numbers := []int{10, 20, 30, 40, 50}
index := 2
fmt.Println("删除前:", numbers)
numbers = append(
numbers[:index],
numbers[index+1:]...,
)
fmt.Println("删除后:", numbers)
fmt.Println("len:", len(numbers))
fmt.Println("cap:", cap(numbers))
}
运行结果:
删除前: [10 20 30 40 50]
删除后: [10 20 40 50]
len: 4
cap: 5
删除过程可以拆成两部分。
目标位置左边:
numbers[:index]
结果:
[10 20]
目标位置右边:
numbers[index+1:]
结果:
[40 50]
然后把右边的元素展开,追加到左边:
append([]int{10, 20}, 40, 50)
最终得到:
[10 20 40 50]
可以记成:
删除位置左边的数据
+
删除位置右边的数据
中间的目标元素自然被跳过。
从前端视角看:类似 splice,但底层表现不同
JavaScript:
const numbers = [10, 20, 30, 40, 50];
numbers.splice(2, 1);
console.log(numbers);
// [10, 20, 40, 50]
Go:
numbers = append(
numbers[:2],
numbers[3:]...,
)
使用目的比较接近,但 Go 的写法更直接地暴露了切片和底层数组之间的关系。
删除以后:
len:5 → 4
cap:仍然是 5
因为这个操作只是缩短了当前 Slice 的长度,并没有让底层数组的存储空间也跟着缩小。
十、删除操作会改写原来的底层数组
接下来又出现了一个很有意思的实验:
package main
import "fmt"
func main() {
original := []int{10, 20, 30, 40, 50}
numbers := original
index := 2
numbers = append(
numbers[:index],
numbers[index+1:]...,
)
fmt.Println("numbers:", numbers)
fmt.Println("original:", original)
fmt.Println("numbers len:", len(numbers))
fmt.Println("numbers cap:", cap(numbers))
}
运行结果:
numbers: [10 20 40 50]
original: [10 20 40 50 50]
numbers len: 4
numbers cap: 5
为什么 original 也发生了变化?
因为:
numbers := original
两个 Slice 仍然共享底层数组。
删除操作:
append(numbers[:2], numbers[3:]...)
会把后面的:
40、50
向前覆盖。
底层数组大概经历了:
原来:[10 20 30 40 50]
覆盖:[10 20 40 50 50]
然后 numbers 的长度变成了 4,所以它只能看到:
[10 20 40 50]
但 original 的长度仍然是 5,所以它看到:
[10 20 40 50 50]
这说明:
Slice 删除元素并不是在底层数组中挖掉一个位置,而是覆盖元素,再缩短当前 Slice 的长度。
十一、不想影响原始数据,就先复制
如果希望删除 numbers 中的数据,但保留 original,可以先创建独立副本:
package main
import "fmt"
func main() {
original := []int{10, 20, 30, 40, 50}
numbers := append([]int{}, original...)
index := 2
numbers = append(
numbers[:index],
numbers[index+1:]...,
)
fmt.Println("numbers:", numbers)
fmt.Println("original:", original)
}
运行结果:
numbers: [10 20 40 50]
original: [10 20 30 40 50]
从前端角度看,这和先使用展开运算符复制数组,再调用 splice 很像:
const original = [10, 20, 30, 40, 50];
const numbers = [...original];
numbers.splice(2, 1);
console.log(numbers);
// [10, 20, 40, 50]
console.log(original);
// [10, 20, 30, 40, 50]
Array、Slice 与 JavaScript Array 对照补充
经过这一轮实验,目前可以整理成:
| Go | JavaScript |
|---|---|
make([]int, 3) | 类似 new Array(3),但 Go 会填充零值 |
make([]int, 0, 5) | 类似空数组,但提前准备了容量 |
len(slice) | array.length |
cap(slice) | 没有直接对应的业务 API |
copy(target, source) | 类似把元素复制到另一个数组 |
append([]int{}, source...) | [...source] |
slice2 := slice1 | 两个变量可能共享底层数据 |
append(slice[:i], slice[i+1:]...) | 用途类似 splice(i, 1) |
Slice 删除后 cap 可能不变 | JS 业务代码通常不关注底层容量 |
| 修改共享底层数组会影响其他 Slice | 修改同一个数组引用会影响其他变量 |
这一阶段最容易踩的几个坑
make 的第二个参数是长度
make([]int, 5)
表示已经存在五个零值元素,不只是提前预留五个位置。
如果准备通过 append 添加数据,更可能需要:
make([]int, 0, 5)
有 cap 不代表可以使用下标
numbers := make([]int, 0, 5)
numbers[0] = 10
仍然会越界,因为:
len = 0
能否使用下标,看的是 len。
copy 不会自动创建目标空间
var target []int
copy(target, source)
此时目标长度是 0,所以复制数量也是 0。
正确复制整个 Slice:
target := make([]int, len(source))
copy(target, source)
Slice 直接赋值不会复制底层数据
copied := original
两个 Slice 很可能仍然共享底层数组。
修改其中一个,也可能影响另一个。
删除元素可能影响其他 Slice
numbers = append(
numbers[:index],
numbers[index+1:]...,
)
这个操作可能复用并改写原来的底层数组。
如果需要保留原始数据,应先复制:
numbers := append([]int{}, original...)
copy 不是任何情况下的深拷贝
对于:
[]int
复制元素以后基本已经独立。
但如果 Slice 中的元素本身还包含引用关系,内部数据仍然可能共享。
不能看到 copy 就直接理解成“所有数据都完全独立”。
写在最后
这一阶段实际完成了:
make 创建 Slice
→ 设置长度
→ 设置容量
→ 提前分配空间
→ append 使用预留容量
→ 容量不足后扩容
→ Slice 直接赋值
→ make + copy 独立复制
→ copy 的复制数量
→ append + ... 快速复制
→ 删除 Slice 元素
→ 删除操作改写底层数组
→ 复制后再删除,保护原始数据
从前端开发的视角看,make 是一个非常容易产生误解的地方。
刚开始可能会把:
make([]int, 0, 5)
简单理解成:
new Array(5)
但实际运行以后才发现:
Go 的 len 是 0
JavaScript 的 length 是 5
Go 只是提前准备了容量,并没有让这五个元素进入当前 Slice。
copy 也让我进一步确认:
Slice 相等或赋值
不代表底层数据已经复制
如果明确不想共享数据,就需要主动创建新的底层空间。
而删除元素的实验则再次提醒我:
numbers = append(
numbers[:index],
numbers[index+1:]...,
)
看起来只是删掉了一个元素,实际上可能直接覆盖原来的底层数组。
这也是学习 Go Slice 后越来越明显的一种感觉:
Slice 表面上很像 JavaScript Array,但 Go 会让开发者更直接地面对长度、容量、底层存储和数据共享关系。
当这些概念真正串起来以后,再回头看:
len
cap
make
append
copy
它们就不再是几个零散的 API,而是一整套围绕底层数组工作的机制。
下一阶段准备进入 Go 的另一个常用集合类型:
Map
接下来要解决的问题是:
Go 的 Map 怎么创建?
怎样新增、修改和删除键值?
访问不存在的键时会发生什么?
怎样判断一个键到底存不存在?
它和 JavaScript Object、Map 又有什么区别?
下一篇见。