一道常见的 Go 面试题是:把切片传给函数,在函数里执行 append,调用方的数据会不会改变?
回答“切片是引用类型,所以会变”,或者“Go 只有值传递,所以不会变”,都不完整。这个细节在参数拼装、权限列表、查询过滤条件中经常造成偶发覆盖。通过几段短代码,就能把它变成开发中真正有用的小技巧。
先回答面试题
先看一个最简单的函数,它试图给列表追加一个元素:
func addTag(tags []string) {
tags = append(tags, "vip")
}
func main() {
tags := []string{"new"}
addTag(tags)
fmt.Println(tags) // [new]
}
调用方没有看到 "vip"。函数接收到的是一份切片描述信息的副本,append 返回的新长度只赋给了函数内的 tags,外层变量的长度仍然是 1。
因此,想让调用方拿到追加后的列表,最稳妥的接口写法是返回新切片:
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。函数参数复制的是这三项信息,而不是整份元素数据。下面的调用方预留了额外容量:
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 就复用底层数组;如果容量不足,则分配新数组并复制原数据。只改一下创建方式,结果就不同:
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
这个坑在拼装查询条件、追加权限和补充标签时非常常见。下面要从基础条件生成管理员与访客两份过滤条件:
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:
import "slices"
func adminFilters(base []string) []string {
out := slices.Clone(base)
return append(out, "role=admin")
}
out 拥有独立数据,后续追加不会覆盖已有结果,也表达了“不修改输入”的意图。较早版本的 Go 也可以用 make 创建同长度切片,通过 copy 复制元素后再追加,效果相同。
代码评审时记住这份清单
遇到接收切片参数的函数,可以快速检查:
- 函数是否执行了
append?如果执行了,是否返回并使用了新切片? - 输入参数是否可能被多个业务分支继续追加?如果会,是否应该先复制?
- 函数语义是修改输入还是生成新结果?生成新结果时应复制数据。
写在最后
切片传参后的 append 并不神秘:函数拿到的是切片头副本,却可能仍与调用方共享底层数组。长度不会自动传回外层,新增元素是否写入原数组,则由容量决定。
实际开发中记住两条规则即可:需要追加结果时,返回切片并接住返回值;需要派生独立列表时,先 slices.Clone 再处理。这个小技巧既能回答面试题,也能避开查询条件和权限列表被意外覆盖的问题。