vault backup: 2026-08-09 19:06:40

This commit is contained in:
2026-08-09 19:06:40 +08:00
parent e2975eb86b
commit 9d664545c9
46 changed files with 7287 additions and 0 deletions
@@ -0,0 +1,199 @@
---
tags: [test/review, go, slice, append-growth, memory-allocation]
create time: 2026-08-09 12:00
---
# 切片底层实现_测试题
## 概述
本测试覆盖 Go 切片的内部结构、nil/empty 区别、append 扩容策略(含 Go 1.18 变化)、三索引切片以及共享底层数组的经典陷阱。共包含 6 道选择题、3 道填空题和 1 道综合简答题。
---
## 一、选择题(6道,由浅入深)
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
### Q1(基础)— 考察定义层面
在 64 位平台上,`unsafe.Sizeof(s)` 对一个类型为 `[]int` 的切片变量 s 返回多少字节?
A. 与切片中元素个数相同,每个元素占 8 字节
B. 永远是 24
C. 是底层数组的总大小(元素个数 × 元素大小)
D. 取决于切片的容量 cap
### Q2(基础)→ 行为判断
以下代码的输出是什么?
```go
package main
import "encoding/json"
import "fmt"
func main() {
var s1 []int
s2 := make([]int, 0)
b1, _ := json.Marshal(s1)
b2, _ := json.Marshal(s2)
fmt.Println(string(b1), string(b2))
}
```
A. `[] []`
B. `null null`
C. `null []`
D. panic
### Q3(进阶)→ 核心原理
Go 1.18 之后,当切片当前容量 `oldCap >= 256` 且触发扩容时,新容量的计算公式是什么?
A. `newCap = oldCap * 2`
B. `newCap = oldCap + oldCap / 2`
C. `newCap = oldCap + oldCap / 4`
D. `newCap = oldCap + 256`
### Q4(进阶)→ 比较与辨析
下面三种切片创建方式中,哪一项描述是正确的?
```go
var s1 []int // 方式一
s2 := make([]int, 0) // 方式二
s3 := []int{} // 方式三
```
A. 方式一的 `s1 == nil` 为 true,方式二和三的 `s2 == nil` 也为 true
B. 方式二和方式三创建的切片,它们的 `ptr` 都指向一块已分配的空数组内存
C. 方式一的 `cap` 不为 0,而是等于后续第一次 append 时的默认值
D. 三者功能完全等价,没有任何语义或行为差异
### Q5(深入)→ 场景推理
```go
orig := []byte("Hello, World!") // len=13, cap=13
short := orig[:5] // short = "Hello", len=5, cap=13
saved := short[:cap(short)] // saved 长度为 13,共享 orig 的底层数组
copy(saved, "Hi") // 将 "Hi" 复制到 saved 的前三个位置
```
执行后 `string(orig)` 的结果是什么?
A. `"Hello, World!"` — orig 不受影响
B. `"Hi, World!"` — orig 前三个字节被覆盖
C. `"Hi"` — orig 长度变为 3
D. panic — 越界访问
### Q6(深入)→ 源码级边界场景
假设有一个空切片 `s := make([]int, 0, 0)`,对其连续调用 `append(s, 1, 2, 3, ..., 100)` 一次性追加 100 个元素。请问最终切片的容量 cap 是多少?
A. 200(翻倍到 ≥ 100)
B. 100(直接使用目标 cap)
C. 128(翻倍两次:0→1→2→...→100 过程中的渐进增长)
D. 101(100 + 预留一个)
---
## 二、填空题(3道)
### F1 — 扩容数值计算
```go
s := make([]int, 0, 300)
// ... 追加若干元素直到触发扩容
// newCap = 300 + 300/4 = _____
```
请计算当 `oldCap = 300` 时首次触发的 `newCap` 值:_____
> **提示**: 使用 Go 1.18 之后的 1.25x 增长率公式,注意整除运算向下取整。
### F2 — 三索引切片容量
```go
a := make([]byte, 5) // a: [0 0 0 0 0], len=5, cap=5
s1 := a[1:4] // s1: _____, len=3, cap=?
s2 := s1[0:2] // s2: [0 0], len=2, cap=?
```
`s1` 的容量 cap = _____;`s2` 受 `s1.cap` 限制,`s2.cap` = _____
> **提示**: 从原始切片 a 切出 a[1:4] 时,max 省略时默认为 a.cap,因此 s1.cap = a.cap - 1。再对 s1 切分时,新切片的 cap 不能超过 s1.cap。
### F3 — 缓存复用模式
```go
var cache []int
// ... 之前已经往 cache 里 append 了不少数据
cache = cache[:0] // 重置长度,保留底层数组
for _, item := range items {
cache = append(cache, process(item))
}
// 下一次调用 cache[:0] 即可复用已分配的内存,无需额外的 _____
```
这种模式中 `cache[:0]` 的核心优势是不触发额外的内存分配,因为底层数组保持不变。空白处填该操作避免的操作名:_____
> **提示**: 对比 `make([]T, expectedSize)` 与 `cache[:0]`,后者跳过了 runtime 层面的哪一步?
---
## 三、简答题(1道)
### S1
你在设计一个 HTTP 请求处理管道,多个 goroutine 并行处理请求并将结果写入同一个 `results` 切片。某次压测发现结果中出现重复数据和脏数据。排查发现:worker goroutine 接收到 request body 的 `[]byte` 缓冲区后直接将其切分传给下游,而没有做 copy。
请结合切片底层的至少三个关键知识点,分析为什么会出现这个问题,并给出修正方案。
> **答题框架提示**:
> 1. 先说明切片字段机制(ptr/len/cap 如何操控共享内存)
> 2. 再说明多个切片共享同一底层数组的后果
> 3. 接着说明 goroutine 并发场景下的具体风险
> 4. 最后给出防御方案(copy 断开共享、预分配等)
---
## 参考答案与解析
### 选择题答案
| 题号 | 正确答案 | 解析 |
|------|---------|------|
| Q1 | B | 切片头在 64 位平台固定为 24 字节(ptr 8 + len 8 + cap 8),与其中元素数量和容量无关。底层数组的实际大小由运行时独立管理,不包含在切片头内。 |
| Q2 | C | `var s1 []int` 创建的是 nil slice(ptr=nil),`encoding/json` 将其序列化为 `null`。`make([]int, 0)` 创建的是 empty slice(有分配底层数组),序列化为 `[]`。这是 nil slice 和 empty slice 最常见的外部可见差异。 |
| Q3 | C | Go 1.18 起,当需求 cap ≥ 256 时使用 `newCap = oldCap + oldCap/4`(即 1.25x 增长率)。小容量(< 256)仍然翻倍。这样做避免了大容量切片过度分配。例如 cap=300 时,newCap=375。 |
| Q4 | B | 方式一 `var s` 的 ptr 为 nil、len=0、cap=0,`s==nil` 为 true。方式二和方式三的 ptr 都指向 runtime 分配的一块空数组(cap≥1),`s==nil` 为 false。方式三比方式二更明确地表达"需要一个非 nil 的空切片"。选项 A 错在方式二三不是 nil;选项 C 错在方式一的 cap=0;选项 D 错在 JSON 序列化等行为确实不同。 |
| Q5 | B | `short` 和 `orig` 共享同一底层数组。`saved := short[:cap(short)]` 将 short 扩展到整个容量(13),然后 `copy(saved, "Hi")` 写入了 `orig[0:3]`,覆盖了原来的 "Hel",结果为 "Hi, World!"。这是一个真实项目中出现过的 bug 模式。 |
| Q6 | B | 根据扩容规则的第 3 步:"如果旧 cap 太小而目标 cap 太大,直接用目标 cap"。从零容量开始一次性 append 100 个元素,target cap 为 100,runtime 直接取 100 而非经过多次翻倍。这是扩容逻辑的特殊分支保护。 |
### 填空题答案
| 题号 | 答案 | 解析 |
|------|------|------|
| F1 | `375` | Go 1.18 扩容公式:`oldCap >= 256` 时用 `newCap = oldCap + oldCap/4 = 300 + 75 = 375`。注意整除运算 `300/4 = 75` 恰好整除。 |
| F2 | `4`, `2` | `a[1:4]` 相当于 `a[1:4:5]`(省略 max 时取原切片 cap),所以 `s1.cap = 5 - 1 = 4`。`s2 := s1[0:2]` 是从 s1 头部切 2 个元素,显式 cap 计算为 `2-0=2`,所以 `s2.cap = 2`。 |
| F3 | `malloc`(或"内存分配") | `cache[:0]` 仅修改 len 字段为 0,不改变 ptr 和 cap,底层数组继续保持可达。下次 append 时可以直接复用已有空间,跳过 runtime 的 malloc 调用和数据拷贝开销。 |
### 简答题参考答案
S1:**参考答案要点**:
1. **切片头部只有 24 字节的指针/长度/容量,真正的数据在共享的底层数组**——多个 worker 传递的切片可能指向同一块内存。worker 之间传递的是切片头而不是数据的副本。
2. **多个 worker 切分同一个 request body 的缓冲区后,它们共享底层数组**——任何一个 worker 对切片的修改(尤其是通过大容量扩展后的 append 操作)都会影响其他 worker 看到的原始数据。
3. **goroutine 并发环境下,request body 缓冲区可能被上游(HTTP handler 或 net/http)复用覆盖**——即使没有直接的相互覆盖,buffer pool 的标准回收机制也会在请求结束后重用这块内存,导致后续 worker 读到被改写的数据。
4. **防御方案**:通过 `copy(dst, src)` 将需要的数据深拷贝到各自独立的底层数组,彻底断开共享关系。对于已知大小的数据,可以先 `make` 预分配再进行 copy。
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。重点考察是否理解"切片是遥控器而非容器"这一核心理念。
## 关联笔记
- [[00.Go/data-structures/Map 底层实现]] — Slice 是 Map bucket 内部的数组类型,理解切片有助于理解 Map 的扩容和数据组织
- [[00.Go/concurrency/Goroutine 调度模型]] — Goroutine 参数传递中使用切片的注意事项
- [[00.Go/runtime/三色标记GC原理]] — 切片作为 GC root 参与可达性分析的过程