首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >面试题:Go 语言中的切片 append 后为什么时变时不变?

面试题:Go 语言中的切片 append 后为什么时变时不变?

作者头像
技术圈
发布2026-05-29 12:23:56
发布2026-05-29 12:23:56
240
举报

一道常见的 Go 面试题是:把切片传给函数,在函数里执行 append,调用方的数据会不会改变?

回答“切片是引用类型,所以会变”,或者“Go 只有值传递,所以不会变”,都不完整。这个细节在参数拼装、权限列表、查询过滤条件中经常造成偶发覆盖。通过几段短代码,就能把它变成开发中真正有用的小技巧。

先回答面试题

先看一个最简单的函数,它试图给列表追加一个元素:

代码语言:javascript
复制
func addTag(tags []string) {
 tags = append(tags, "vip")
}

func main() {
 tags := []string{"new"}
 addTag(tags)
 fmt.Println(tags) // [new]
}

调用方没有看到 "vip"。函数接收到的是一份切片描述信息的副本append 返回的新长度只赋给了函数内的 tags,外层变量的长度仍然是 1

因此,想让调用方拿到追加后的列表,最稳妥的接口写法是返回新切片:

代码语言:javascript
复制
func addTag(tags []string) []string {
 return append(tags, "vip")
}

func main() {
 tags := []string{"new"}
 tags = addTag(tags)
 fmt.Println(tags) // [new vip]
}

实用规则很直接:函数内可能执行 append 时,应返回结果,并让调用方接住返回值。 但即使外层长度没有变化,底层数据也可能已被修改。

切片里到底装了什么

切片可以理解为三项信息:底层数组位置、当前长度 len、容量 cap。函数参数复制的是这三项信息,而不是整份元素数据。下面的调用方预留了额外容量:

代码语言:javascript
复制
func addHidden(nums []int) {
 nums = append(nums, 99)
}

func main() {
 nums := make([]int, 1, 3)
 nums[0] = 10
 addHidden(nums)
 fmt.Println(nums, nums[:2]) // [10] [10 99]
}

输出中的 nums 仍是 [10],但重新切到长度 2 后,99 出现了:函数里的追加操作复用了原底层数组。调用方看不到新长度,却可能承受数组被写入的副作用。

容量决定是否共用数组

按照 Go 语言规范,如果容量足够,append 就复用底层数组;如果容量不足,则分配新数组并复制原数据。只改一下创建方式,结果就不同:

代码语言:javascript
复制
func addHidden(nums []int) {
 nums = append(nums, 99)
}

func main() {
 nums := []int{10}
 addHidden(nums)
 fmt.Println(nums, cap(nums)) // [10] 1
}

此时容量只有 1,追加元素已经装不下,函数内会使用新数组。这就是相同函数“有时影响输入、有时不影响”的原因:差异可能只来自调用方的容量。

面试时可以这样总结:

  • items[0] = x 这样的原位修改,调用方通常可见。
  • append 后,调用方的 len 不会自动增加。
  • append 是否写入原数组,取决于容量是否足够。
  • 追加后的切片必须通过返回值交还调用方。

实际业务中的隐蔽 Bug

这个坑在拼装查询条件、追加权限和补充标签时非常常见。下面要从基础条件生成管理员与访客两份过滤条件:

代码语言:javascript
复制
func filters(base []string, v string) []string { return append(base, v) }
func main() {
 base := make([]string, 1, 4)
 base[0] = "enabled=1"
 admin := filters(base, "role=admin")
 _ = filters(base, "role=guest")
 fmt.Println(admin) // [enabled=1 role=guest]
}

两次追加写进了同一个数组位置,访客条件覆盖了管理员条件。两个返回值看似独立,实际可能共享数据;只要多条业务分支从同一个切片继续追加,就需要留意这个问题。

需要独立数据时主动复制

如果函数的语义是“基于输入生成新列表”,就应该先复制再追加。Go 1.21 及以上版本可以直接使用标准库 slices.Clone

代码语言:javascript
复制
import "slices"

func adminFilters(base []string) []string {
 out := slices.Clone(base)
 return append(out, "role=admin")
}

out 拥有独立数据,后续追加不会覆盖已有结果,也表达了“不修改输入”的意图。较早版本的 Go 也可以用 make 创建同长度切片,通过 copy 复制元素后再追加,效果相同。

代码评审时记住这份清单

遇到接收切片参数的函数,可以快速检查:

  • 函数是否执行了 append?如果执行了,是否返回并使用了新切片?
  • 输入参数是否可能被多个业务分支继续追加?如果会,是否应该先复制?
  • 函数语义是修改输入还是生成新结果?生成新结果时应复制数据。

写在最后

切片传参后的 append 并不神秘:函数拿到的是切片头副本,却可能仍与调用方共享底层数组。长度不会自动传回外层,新增元素是否写入原数组,则由容量决定。

实际开发中记住两条规则即可:需要追加结果时,返回切片并接住返回值;需要派生独立列表时,先 slices.Clone 再处理。这个小技巧既能回答面试题,也能避开查询条件和权限列表被意外覆盖的问题。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 技术圈子 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先回答面试题
  • 切片里到底装了什么
  • 容量决定是否共用数组
  • 实际业务中的隐蔽 Bug
  • 需要独立数据时主动复制
  • 代码评审时记住这份清单
  • 写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档