Golang递归中传递结构体引用遇到的问题

Golang递归中传递结构体引用遇到的问题 大家好,我来自阿拉伯联合酋长国(阿联酋)迪拜,遇到了一个递归函数的问题,希望能得到一些关于可能出错原因的建议或意见。

以下是情况的简要概述:

我有一个递归函数 play(b),其中 b 是一个包含两个切片(sw)的结构体。这些切片包含具有几个字段的结构体:string1string2bool

play(b) 函数内部,有一段代码如下:

bNew := mm(bIn)
bOrigIn := bIn
play(bNew)

play() 会递归调用自身,最终递归会返回。我看到的问题是,在从某些递归层级返回后,bInbOrigIn 都与递归调用前的状态不同,即使它们本应保持不变。

问题:

  • 函数达到某个递归深度(例如 23 层),然后继续深入另外 10 层。
  • 当返回到第 23 层时,bIn 已从其原始值改变。奇怪的是,bOrigIn 也以与 bIn 完全相同的方式发生了变化(这表明它们以某种方式关联在一起)。
  • 然而,bNew 如预期保持不变。

补充细节:

  • bIn 包含切片 sw,每个切片有 9 个元素。
  • 当返回到第 23 层时,切片 w 的第 9 个元素已被修改。具体来说,它现在包含了切片 s 第 9 个元素的值,但布尔值被翻转了,并且 s 的第 9 个元素被移除了。
  • 这个修改是由递归过程中的某个时刻调用的函数 mm(bIn) 完成的。

核心问题:

  1. 为什么 bInbOrigIn 会改变,它们本应是局部变量?

    • 既然 bInbOrigIn 都是函数级别的变量(非包级别),为什么它们似乎会受到递归中发生的变化的影响?
  2. 这是切片问题吗?

    • 我怀疑由于 Go 的切片是引用类型(即它们指向同一个底层数组),递归中所做的更改可能会同时影响 bInbOrigIn,因为它们共享对相同内存位置的引用。如果是这种情况,这是否解释了为什么两者都被修改,而本应只有其中一个被修改?
  3. 我应该注意什么来防止这种情况?

    • 在 Go 中处理切片和递归时,是否有任何最佳实践可以避免此类意外的副作用?具体来说,在将切片(或结构体)传递给像 mm() 这样的函数之前,我是否应该复制它们,以确保更改不会意外传播?

我尝试过的:

  • 我尝试过使用 :== 来创建 bOrigIn := bIn,但问题仍然存在。
  • 我知道 Go 中的切片是引用类型,因此除非显式复制,否则在函数中对一个切片的任何修改都可能影响原始切片。

寻求建议:

如果有人对如何处理这种情况或如何避免这类切片引用问题有建议,我将非常感谢您的想法!

感谢阅读! Emmanuel Katto


更多关于Golang递归中传递结构体引用遇到的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

在未看到实际代码的情况下,很难准确指出你的具体问题。

但一般来说,你是对的。像切片(slice)和映射(map)这样的几种类型,实际上是经过修饰的指针。因此,当你使用赋值语句 a := b 时,你是在复制指针,它仍然指向同一个值。如果你想复制切片的内容,你必须使用:

b := make([]whatever, len(a))
copy(b,a)

但是请注意——这种复制只有一层的深度。例如,如果切片是一个指向指针、映射或可能包含指针的结构体的切片,那么这些引用将不会被复制。如果你也想复制这些值,就必须使用递归复制。

更多关于Golang递归中传递结构体引用遇到的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我怀疑由于 Go 的切片是引用类型(即它们指向同一个底层数组)

切片是值类型;但它们持有一个指向数组的指针。根据这个回答,切片头看起来是这样的:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

来自 Go 语言之旅

切片就像数组的引用

切片不存储任何数据,它只是描述底层数组的一个部分。

更改切片的元素会修改其底层数组的相应元素。

共享同一底层数组的其他切片将会看到这些更改。

这里有一篇 Dave Cheney 写的很好的博客文章:

https://dave.cheney.net/2018/07/12/slices-from-the-ground-up

这是一个典型的切片引用问题。在Go中,切片是引用类型,当你在递归中传递包含切片的结构体时,实际上传递的是对底层数组的引用。

问题分析:

  1. bOrigIn := bIn 创建的是结构体的浅拷贝,切片字段仍然指向相同的底层数组
  2. mm(bIn) 函数可能修改了切片的底层数据
  3. 递归调用中的修改会影响到所有共享相同底层数组的变量

示例代码说明问题:

package main

import "fmt"

type Data struct {
    S []int
    W []int
}

func mm(d Data) Data {
    // 修改切片元素 - 这会影响到原始数据
    if len(d.S) > 0 {
        d.S[0] = 999
    }
    return d
}

func play(d Data, depth int) {
    if depth >= 3 {
        return
    }
    
    fmt.Printf("Depth %d - Before: d.S[0] = %d\n", depth, d.S[0])
    
    bNew := mm(d)
    bOrigIn := d  // 浅拷贝,切片仍然共享底层数组
    
    fmt.Printf("Depth %d - After mm: d.S[0] = %d, bOrigIn.S[0] = %d\n", 
        depth, d.S[0], bOrigIn.S[0])
    
    play(bNew, depth+1)
    
    fmt.Printf("Depth %d - After recursion: d.S[0] = %d, bOrigIn.S[0] = %d\n", 
        depth, d.S[0], bOrigIn.S[0])
}

func main() {
    d := Data{
        S: []int{1, 2, 3},
        W: []int{4, 5, 6},
    }
    play(d, 0)
}

输出会显示所有变量都被修改了,因为它们共享相同的底层数组。

解决方案:深度拷贝结构体

import "golang.org/x/exp/slices"

func deepCopyData(d Data) Data {
    return Data{
        S: slices.Clone(d.S),  // Go 1.21+ 或使用 exp/slices
        W: slices.Clone(d.W),
    }
}

// 或者手动实现
func deepCopyDataManual(d Data) Data {
    sCopy := make([]int, len(d.S))
    wCopy := make([]int, len(d.W))
    copy(sCopy, d.S)
    copy(wCopy, d.W)
    return Data{
        S: sCopy,
        W: wCopy,
    }
}

// 在递归中使用
func play(d Data, depth int) {
    if depth >= 3 {
        return
    }
    
    // 创建深度拷贝
    bNew := deepCopyData(d)
    bNew = mm(bNew)  // 现在修改不会影响原始数据
    
    bOrigIn := deepCopyData(d)  // 深度拷贝保持原始状态
    
    play(bNew, depth+1)
    
    // 现在 d 和 bOrigIn 保持不变
}

对于包含结构体切片的深度拷贝:

type Item struct {
    String1 string
    String2 string
    Bool    bool
}

type Container struct {
    S []Item
    W []Item
}

func deepCopyContainer(c Container) Container {
    sCopy := make([]Item, len(c.S))
    wCopy := make([]Item, len(c.W))
    
    for i := range c.S {
        sCopy[i] = c.S[i]  // Item 是值类型,会被拷贝
    }
    
    for i := range c.W {
        wCopy[i] = c.W[i]
    }
    
    return Container{
        S: sCopy,
        W: wCopy,
    }
}

关键点:

  1. Go中的切片赋值是浅拷贝,只拷贝切片头(指针、长度、容量)
  2. 结构体赋值会拷贝所有字段,但对于切片字段只拷贝切片头
  3. 递归中需要显式进行深度拷贝来隔离数据修改
  4. 使用 copy() 函数或 slices.Clone() 来复制切片内容
回到顶部