Golang中多维切片追加元素的意外行为解析

Golang中多维切片追加元素的意外行为解析 我发现向切片追加元素时出现了一个奇怪的行为:

为什么

package main

import (
	"fmt"
)

var (
	a  []int
	aa [][]int
)

func main() {
	a = make([]int, 4, 4)

	a[0] = 1
	a[1] = 2
	a[2] = 3
	a[3] = 4

	for i := 0; i < 4; i++ {
		a[0] = a[0] * 2
		a[1] = a[1] * 3
		a[2] = a[2] * 4
		a[3] = a[3] * 5

		aa = append(aa, []int{a[0], a[1], a[2], a[3]}) //[[2 6 12 20] [4 18 48 100] [8 54 192 500] [16 162 768 2500]]
		//aa = append(aa, a) //[[16 162 768 2500] [16 162 768 2500] [16 162 768 2500] [16 162 768 2500]]
		//aa = append(aa, a[:]) //[[16 162 768 2500] [16 162 768 2500] [16 162 768 2500] [16 162 768 2500]]
	}

	fmt.Println(aa)
}

会得到结果

[[2 6 12 20] [4 18 48 100] [8 54 192 500] [16 162 768 2500]]

然而,追加整个切片

…
aa = append(aa, a)
…

却得到结果

[[16 162 768 2500] [16 162 768 2500] [16 162 768 2500] [16 162 768 2500]]

?

甚至通过复制切片

…
aa = append(aa, a[:])
…

也会导致这种恼人的行为。

如果不是一个错误,那至少也是一个非常糟糕的特性!


更多关于Golang中多维切片追加元素的意外行为解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

如果不使用 make 创建一个新的切片,那么你将反复共享同一个切片。

func main() {
    fmt.Println("hello world")
}

更多关于Golang中多维切片追加元素的意外行为解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


感谢您的详细解释!

在这种情况下,这是一个不那么快捷且更“粗糙”的解决方案:

…
var b []int
b = make([]int, 4, 4)
for i := 0; i < len(a); i++ {
	b[i] = a[i]
}

aa = append(aa, b)
…

但这并不直观,并且绝对是难以发现的错误来源。

是的,最终结果是 aa 中的每个元素都指向同一个 a 的实例。 因此,如果你将 a[0] 的值加倍,那么 aa[0][0]aa[1][0]aa[2][0]aa[3][0] 的值也都会加倍,因为它们都指向同一个内存地址。这就是切片的工作原理。

请参阅 Go Slices: usage and internals - The Go Programming Language

我曾尝试过

copy(b,a)

同样,但我犯了一个错误,只分配了一次内存,并在循环内重新赋值。这没有奏效:

for … {
	…
	a[0] = a[0] * 2
	…
	b := make([]int, 4, 4)
	copy(b, a)
	aa = append(aa, b)
	…
	}

你的方法成功了,但变量 b 必须每次都重新分配。

b := make([]int, 4, 4)
for … {
	…
	a[0] = a[0] * 2
	…
	copy(b, a)
	aa = append(aa, b)
	…
	}

这个行为不是错误,而是切片底层数组共享导致的预期行为。当您使用 append(aa, a)append(aa, a[:]) 时,实际上是在向 aa 追加指向同一个底层数组的切片引用。

以下是关键点解析:

  1. 切片是引用类型:切片本身不存储数据,而是指向一个底层数组
  2. aa[:] 指向相同的底层数组:两者都引用相同的存储空间
  3. 循环中修改 a 会影响所有已追加的切片:因为所有追加的切片都指向同一个底层数组

示例代码演示了这个问题:

package main

import "fmt"

func main() {
    var aa [][]int
    a := make([]int, 4, 4)
    
    for i := 0; i < 4; i++ {
        // 填充初始值
        for j := 0; j < 4; j++ {
            a[j] = (i+1)*(j+1)
        }
        
        // 错误方式:追加同一个切片的引用
        aa = append(aa, a) // 所有元素都指向同一个底层数组
        
        // 正确方式:创建新切片
        // aa = append(aa, []int{a[0], a[1], a[2], a[3]})
    }
    
    // 修改原始切片
    a[0] = 999
    
    // 打印结果 - 所有元素都显示修改后的值
    fmt.Println(aa) // [[999 2 3 4] [999 2 3 4] [999 2 3 4] [999 2 3 4]]
}

要解决这个问题,需要在每次迭代时创建新的切片副本:

package main

import "fmt"

func main() {
    var aa [][]int
    a := make([]int, 4, 4)
    
    for i := 0; i < 4; i++ {
        // 填充值
        for j := 0; j < 4; j++ {
            a[j] = (i+1)*(j+1)
        }
        
        // 方法1:显式创建新切片
        newSlice := make([]int, len(a))
        copy(newSlice, a)
        aa = append(aa, newSlice)
        
        // 方法2:使用切片字面量(编译器会创建新数组)
        // aa = append(aa, []int{a[0], a[1], a[2], a[3]})
        
        // 方法3:使用 append 复制
        // aa = append(aa, append([]int{}, a...))
    }
    
    // 修改原始切片不会影响 aa 中的元素
    a[0] = 999
    fmt.Println(aa) // 每个元素保持各自的值
}

这种行为设计是为了提高性能,避免不必要的内存分配和复制。理解切片底层数组的共享机制对于正确使用Go语言至关重要。

回到顶部