Go 语言中切片(slice)行为的问题

package main

import "fmt"

func main() {
case1()
}

func case1() {
s := make([]int, 3)
s[0] = 1
s[1] = 2
s[2] = 3

fmt.Printf("%p,%v\n", &s[0], s)

appendToSlice(s)

fmt.Println(s) // 输出 [1 2 3],未改变

}

func appendToSlice(s []int) {
s[0] = 100
fmt.Printf("%p,%v\n", &s[0], s)
s = append(s, 100) // 修改局部切片,不会影响原始切片
fmt.Printf("%p,%v\n", &s[0], s)
}

你的问题是一个非常经典的 Go 语言中切片(slice)行为的问题。让我们一步步分析为什么 s = append(s, 100) 修改了局部切片,却没有影响原始切片,同时结合你的代码输出进行解释。


代码运行结果

先运行你的代码,假设输出类似以下(具体地址会因运行环境不同而变化):

0xc0000b2000,[1 2 3]       // main 中原始切片的地址和值
0xc0000b2000,[100 2 3]     // appendToSlice 中 s[0] = 100 后的地址和值
0xc0000b2040,[100 2 3 100] // appendToSlice 中 append 后的地址和值
[100 2 3]                  // main 中最终的 s

从输出可以看到:

  1. s[0] = 100 修改了原始切片的内容(地址没变,值变为 [100 2 3])。
  2. s = append(s, 100) 追加了元素,但地址变了,最终没有影响 main 中的 s

关键问题:为什么 append 不影响原始切片?

1. 切片的底层结构

切片在 Go 中是一个结构体,包含三个字段:

  • 指针:指向底层数组的起始位置。
  • 长度 (len):当前切片包含的元素个数。
  • 容量 (cap):底层数组的总容量。

当你将切片 s 传递给 appendToSlice 函数时,传递的是这个结构体的副本。副本中的指针仍然指向原始底层数组,因此修改 s[0] 会影响原始数据。但如果切片本身被重新赋值(比如通过 append),只会修改局部副本,不会影响调用者的切片。

2. append 的行为

  • append 函数会向切片追加元素。
  • 如果当前切片的容量(cap)足够,append 会在底层数组中追加元素,并返回更新后的切片(长度增加)。
  • 如果容量不足,append 会分配一个新的、更大的底层数组,将原有数据复制过去,然后追加新元素,并返回新的切片。

在你的代码中:

  • 初始切片 s := []int{1, 2, 3} 的长度是 3,容量也是 3(默认情况下,[]int{} 的容量等于初始长度)。
  • 调用 s = append(s, 100) 时,长度需要从 3 增加到 4,但容量只有 3,不够用。
  • 因此,append 创建了一个新的底层数组(地址变为 0xc0000b2040),将 [100 2 3] 复制过去,再追加 100,结果是 [100 2 3 100]
  • 这个新切片被赋值给函数内的局部变量 s,但原始的 s(在 main 中)没有被修改。

3. 函数参数是值传递

Go 中所有的参数传递都是值传递。对于切片来说,传递的是切片结构体的副本,而不是底层数组的副本。因此:

  • 修改切片的内容(如 s[0] = 100)会影响底层数组,进而影响原始切片(因为指针指向同一个数组)。
  • 修改切片本身(如 s = append(s, 100))只会影响局部变量 s,不会影响调用者的 s,因为这是对副本的重新赋值。

逐步分析代码

初始状态

s := []int{1, 2, 3}
// s: {ptr: 0xc0000b2000, len: 3, cap: 3}
  • 底层数组:[1 2 3]
  • 地址:0xc0000b2000

调用 appendToSlice(s)

  • 传递 s 的副本给函数,函数内的 s 初始状态与 main 中的 s 相同:
    • s: {ptr: 0xc0000b2000, len: 3, cap: 3}

执行 s[0] = 100

s[0] = 100
fmt.Printf("%p,%v\n", &s[0], s)
// 输出: 0xc0000b2000,[100 2 3]
  • 修改底层数组的第一个元素,变为 [100 2 3]
  • 地址不变,因为操作的是底层数组的内容。

执行 s = append(s, 100)

s = append(s, 100)
fmt.Printf("%p,%v\n", &s[0], s)
// 输出: 0xc0000b2040,[100 2 3 100]
  • 容量不足(len=3, cap=3),append 创建新数组。
  • 新数组内容:[100 2 3 100]
  • 新切片 s: {ptr: 0xc0000b2040, len: 4, cap: 4(或更大)}
  • 局部变量 s 被更新为新切片,但 main 中的 s 保持不变。

返回到 main

fmt.Println(s)
// 输出: [100 2 3]
  • main 中的 s 仍然是 {ptr: 0xc0000b2000, len: 3, cap: 3}
  • 底层数组被修改为 [100 2 3],但未追加 100,因为 append 的结果没有传回。

如何让 append 影响原始切片?

有两种常见方法:

  1. 返回新切片并赋值
func appendToSlice(s []int) []int {
    s = append(s, 100)
    return s
}

func main() {
    s := []int{1, 2, 3}
    s = appendToSlice(s)
    fmt.Println(s) // 输出 [1 2 3 100]
}
  1. 使用指针传递切片
func appendToSlice(s *[]int) {
    *s = append(*s, 100)
}

func main() {
    s := []int{1, 2, 3}
    appendToSlice(&s)
    fmt.Println(s) // 输出 [1 2 3 100]
}

总结

  • s = append(s, 100) 不影响原始切片的原因是:
    1. append 返回的新切片只赋值给了函数内的局部变量 s
    2. 当容量不足时,append 创建了新的底层数组,局部 s 的指针指向新地址,但原始 s 未变。
  • 修改切片内容(如 s[0] = 100)会影响原始数据,因为底层数组是共享的。
  • 要让 append 的结果影响原始切片,必须通过返回值或指针显式传递回去。

希望这个解释清楚地解答了你的疑问!如果还有其他问题,欢迎继续讨论。

posted @ 2025-03-05 13:39  仁义礼智信的  阅读(30)  评论(0)    收藏  举报