vault backup: 2026-06-07 12:14:39

This commit is contained in:
2026-06-07 12:14:39 +08:00
parent 1a72bc4d82
commit 7ce9b83218
61 changed files with 8409 additions and 14429 deletions
+97 -149
View File
@@ -1,195 +1,143 @@
---
tags:
- Go
- golang
- 切片
- 面试题
- slice面试题
tags: [go, golang, interview, slice-questions]
create time: 2026-06-07 14:30
---
# Slice面试题
# Slice 面试题 🍕
## 1. slice的底层结构是怎样的?
## 概述
slice 的底层数据其实也是数组,slice 是对数组的封装,它描述一个数组的片段。slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。
本文件涵盖 Go Slice 的 4 道高频面试题,涉及底层结构、扩容机制、截取共享和函数传参等核心考点。Slice 是 Go 中最常用的数据结构之一,也是面试中的"必考题"。
## 关联笔记
- [[hzh/GolangStar/Go语言基础/Go语言数组与切片]] — Slice 基础用法
- [[hzh/GolangStar/Go语言原理/slice原理]] — Slice 底层扩容策略详解
- [[hzh/GolangStar/Go面试题库/内存管理面试题]] — Slice 引用大数组导致的内存泄漏
## 正文
### Q1:slice 的底层结构是怎样的? 🟢简单
> [!question] ❓ 思考一下
> 如果让你设计一个能动态增长的数组,你会在结构体中放哪些字段?
## 参考答案
slice 是对数组的封装,描述一个数组的片段。其底层是一个包含三个字段的结构体:
```go
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
array unsafe.Pointer // 指向底层数组的指针
len int // 长度(当前元素个数)
cap int // 容量(底层数组从起始位置可容纳的元素数)
}
```
![](https://golangstar.cn/assets/img/go语言系列/go面试题库/Slice面试题/image.png)
> [!note] 📝 核心考点
> **len ≠ cap** 是理解 slice 的关键。len 是你"能看到"的元素数量,cap 是底层数组的实际容量。当 len == cap 时,append 会触发扩容。
## 2. Go语言里slice是怎么扩容的?
---
1.17及以前
### Q2:slice 是怎么扩容的? 🟡中等
1. 如果期望容量大于当前容量的两倍就会使用期望容量;
> [!question] ❓ 思考一下
> 如果一个 slice 的 cap 是 10,你 append 第 11 个元素时会发生什么?扩容后的 cap 是多少?
2. 如果当前切片的长度小于 1024 就会将容量翻倍;
## 参考答案
3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
#### Go 1.17 及以前
Go1.18及以后,引入了新的扩容规则:
| 条件 | 扩容规则 |
|------|---------|
| 期望容量 > 当前容量的 2 倍 | 使用期望容量 |
| len < 1024 | 容量翻倍(×2) |
| len >= 1024 | 容量增加 25%(×1.25),直到 >= 期望容量 |
当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3\*256)/4
#### Go 1.18 及以后(新规则)
## 3. 从一个切片截取出另一个切片,修改新切片的值会影响原来的切片内容吗
| 条件 | 扩容公式 |
|------|---------|
| oldcap < 256 | newcap = oldcap × 2 |
| oldcap >= 256 | newcap = oldcap + (oldcap + 3×256) / 4 |
在截取完之后,如果新切片没有触发扩容,则修改切片元素会影响原切片,如果触发了扩容则不会。
> [!warning] ⚠️ 高频陷阱
> Go 1.18 的扩容规则更保守,减少了过度分配内存的情况。面试中如果不确定版本,可以先说旧规则再补充新规则的变化。
示例:
> [!info] 🔗 延伸阅读
> - [[hzh/GolangStar/Go语言原理/slice原理]] — 详细的扩容策略源码分析
---
### Q3:切片截取后修改会影响原切片吗? 🟡中等
> [!question] ❓ 思考一下
> `s2 := s1[2:6:7]`,然后对 s2 做 append,什么时候会影响 s1,什么时候不会?
## 参考答案
**关键判断标准**:新切片是否触发了扩容。
```go
package main
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5] // len=3, cap=8, 底层指向 slice[2]
s2 := s1[2:6:7] // len=4, cap=5, 底层指向 slice[4]
import "fmt"func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]
s2 = append(s2, 100) // 容量够,直接追加 → 影响底层数组
s2 = append(s2, 200) // 容量不够,触发扩容 → 创建新数组,不影响 s1
s2 = append(s2, 100)
s2 = append(s2, 200)
s1[2] = 20 // 只影响 s1 可见范围
s1[2] = 20
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(slice)
}
fmt.Println(s1) // [2 3 20]
fmt.Println(s2) // [4 5 6 7 100 200]
fmt.Println(slice) // [0 1 2 3 20 5 6 7 100 9]
```
运行结果:
**解析思路:**
1. 第一次 append(100):s2 cap=5,追加后 len=5,未超 cap,直接写入底层数组 → s1 能看到变化
2. 第二次 append(200):s2 cap 已满,触发扩容创建新底层数组 → s1 看不到变化
3. 修改 s1[2]:通过原始底层数组修改,但 s2 已有独立数组 → s2 不受影响
```shell
[2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]
```
> [!tip] 💡 面试技巧
> 回答这道题时,先画出三张图展示每次操作后的底层状态变化。面试官最看重的不是背答案,而是你能否清晰地描述数据流向。
`s1` 从 `slice` 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。 `s2` 从 `s1` 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。
---
![](https://golangstar.cn/assets/img/go语言系列/go面试题库/Slice面试题/image-1.png)
### Q4:slice 作为函数参数会改变原 slice 吗? 🟡中等
接着,向 `s2` 尾部追加一个元素 100:
> [!question] ❓ 思考一下
> 在函数里 `append(s, 100)` 后返回新的 slice,外层的 s 会变吗?为什么?
## 参考答案
**分两种情况:**
| 操作 | 外层 slice 结构变吗? | 底层数据变吗? |
|------|---------------------|--------------|
| 修改元素值 `s[i] = x` | 否(传的是副本) | **是**(共享底层数组) |
| append 后赋值给 s | 否 | 否(s 只是副本) |
| 传递 `*[]int` 指针 | **是** | **是** |
```go
s2 = append(s2, 100)
```
`s2` 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 `s1` 都可以看得到。
![](https://golangstar.cn/assets/img/go语言系列/go面试题库/Slice面试题/image-2.png)
再次向 `s2` 追加元素200
```go
s2 = append(s2, 200)
```
这时,`s2` 的容量不够用,该扩容了。于是,`s2` 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 `append` 带来的再一次扩容,`s2` 会在此次扩容的时候多留一些 `buffer`,将新的容量将扩大为原始容量的2倍,也就是10了。
![](https://golangstar.cn/assets/img/go语言系列/go面试题库/Slice面试题/image-3.png)
最后,修改 `s1` 索引为2位置的元素:
```go
s1[2] = 20
```
这次只会影响原始数组相应位置的元素。它影响不到 `s2` 了,人家已经远走高飞了。
![](https://golangstar.cn/assets/img/go语言系列/go面试题库/Slice面试题/image-4.png)
再提一点,打印 `s1` 的时候,只会打印出 `s1` 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。
## 4. slice作为函数参数传递,会改变原slice吗?
当 slice 作为函数参数时,因为会拷贝一份新的slice作为实参,所以原来的 slice 结构并不会被函数中的操作改变,也就是说,slice 其实是一个结构体,包含了三个成员:len, cap, array并不会变化。但是需要注意的是,尽管slice结构不会变,但是其底层数组的数据如果有修改的话,则会发生变化。若传的是 slice 的指针,则原 slice 结构会变,底层数组的数据也会变。
示例:
```go
package main
func main() {
s := []int{1, 1, 1}
f(s)
fmt.Println(s)
}
func f(s []int) {
// i只是一个副本,不能改变s中元素的值
/*for _, i := range s {
i++
}
*/
for i := range s {
s[i] += 1
}
}
```
程序输出:
```go
[2 2 2]
```
果真改变了原始 slice 的底层数据。这里传递的是一个 slice 的副本,在 `f` 函数中,`s` 只是 `main` 函数中 `s` 的一个拷贝。在`f` 函数内部,对 `s` 的作用并不会改变外层 `main` 函数的 `s`的结构。
要想真的改变外层 `slice`,只有将返回的新的 slice 赋值到原始 slice,或者向函数传递一个指向 slice 的指针。我们再来看一个例子:
```go
package main
import "fmt"
// 方法1:返回新 slice(推荐)
func myAppend(s []int) []int {
// 这里 s 虽然改变了,但并不会影响外层函数的 s
s = append(s, 100)
return s
return append(s, 100)
}
s = myAppend(s) // 需要重新赋值
// 方法2:传递指针
func myAppendPtr(s *[]int) {
// 会改变外层 s 本身
*s = append(*s, 100)
return
*s = append(*s, 100)
}
func main() {
s := []int{1, 1, 1}
newS := myAppend(s)
fmt.Println(s)
fmt.Println(newS)
s = newS
myAppendPtr(&s)
fmt.Println(s)
}
myAppendPtr(&s) // 直接修改
```
程序输出
> [!note] 📝 核心考点
> slice 本身是结构体(三个字段),传入函数时被复制了一份。函数内修改**底层数组的数据**会影响外部,但**修改 slice 结构本身**(如让它的 array 指向别处)不会影响外部。
```go
[1 1 1]
[1 1 1 100]
[1 1 1 100 100]
```
## 关联笔记
`myAppend` 函数里,虽然改变了 `s`,但它只是一个值传递,并不会影响外层的 `s`,因此第一行打印出来的结果仍然是 `[1 1 1]`。
而 `newS` 是一个新的 `slice`,它是基于 `s` 得到的。因此它打印的是追加了一个 `100` 之后的结果: `[1 1 1 100]`。
最后,将 `newS` 赋值给了 `s`,`s` 这时才真正变成了一个新的slice。之后,再给 `myAppendPtr` 函数传入一个 `s 指针`,这回它真的被改变了:`[1 1 1 100 100]`
- [[hzh/GolangStar/Go语言基础/Go语言数组与切片]]
- [[hzh/GolangStar/Go语言原理/slice原理]]
- [[hzh/GolangStar/Go面试题库/内存管理面试题]]