Files

9.0 KiB
Raw Permalink Blame History

tags, create time
tags create time
test/review
go
slice
append-growth
memory-allocation
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(基础)→ 行为判断

以下代码的输出是什么?

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(进阶)→ 比较与辨析

下面三种切片创建方式中,哪一项描述是正确的?

var s1 []int       // 方式一
s2 := make([]int, 0)  // 方式二
s3 := []int{}          // 方式三

A. 方式一的 s1 == nil 为 true,方式二和三的 s2 == nil 也为 true
B. 方式二和方式三创建的切片,它们的 ptr 都指向一块已分配的空数组内存
C. 方式一的 cap 不为 0,而是等于后续第一次 append 时的默认值
D. 三者功能完全等价,没有任何语义或行为差异

Q5(深入)→ 场景推理

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 — 扩容数值计算

s := make([]int, 0, 300)
// ... 追加若干元素直到触发扩容
// newCap = 300 + 300/4 = _____

请计算当 oldCap = 300 时首次触发的 newCap 值:_____

提示: 使用 Go 1.18 之后的 1.25x 增长率公式,注意整除运算向下取整。

F2 — 三索引切片容量

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 — 缓存复用模式

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 个要点即可得满分;完全正确需覆盖全部要点。重点考察是否理解"切片是遥控器而非容器"这一核心理念。

关联笔记