Golang数据结构设计:应该返回内部元素还是副本?语言特性强制返回副本!

Golang数据结构设计:应该返回内部元素还是副本?语言特性强制返回副本! 大家好,

我正在实现我的树数据结构。其中将存放任何实现了我的 Value 接口类型的元素。

我有一个函数,用于返回树中最大的元素:

func (t *tree) Largest() *Value

在这个函数内部,我首先计算最大元素的值:

largest := current

然后,最后一行代码是:

return &largest

我遍历这棵树,找到其最大元素,然后复制它并返回指向该副本的指针。语法 largest := 会复制实际的元素。返回其地址就是返回副本的地址。

这样做有一个优点:如果我在数据结构中存放复杂值,那么我得到的是副本,修改副本可以确保原始值保持不变——从而减少错误。

但它也有一个缺点:对于大数据,如果我要存储大量元素并检索每个元素,我将使用双倍的内存大小,因为每次检索都会分配一个副本。

这里惯用的做法是什么?我应该坚持返回副本的指针,还是尝试返回实际插入的原始元素?

在生产环境中:我该如何记录这个函数返回的是副本,而那个函数返回的是实际的原始对象——以便我记得它类似于不可变对象——这里有最佳实践吗?有人记录关于他们返回值的这个细节吗?


更多关于Golang数据结构设计:应该返回内部元素还是副本?语言特性强制返回副本!的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我会在其中放入任何实现了我的 Value 接口的类型的元素。

如果 Value 是一个接口,那么类型为 Value 的变量(或结构体字段)包含两个指针:一个指向类型,一个指向装箱的值。因为 Value 已经是指向包装值的指针,所以你不应该返回指向 Value 的指针。直接按值返回即可。

这样做有一个优点:如果我在数据结构中放入复杂的值,我得到的是副本,修改副本可以确保原始值保持不变——这样我遇到的错误会更少。

无论你返回 *Value 还是 Value,封装在 Value 接口内部的值要么是指针,要么是值。如果它是指针,那么你的 API 的使用者仍然有能力修改树中的值。

但它也有一个缺点:对于大数据,如果我要存储大量元素并检索每个元素,我将使用双倍的内存大小,因为每次检索都会分配一个副本。

就像我之前说的,如果 Value 是一个接口,那么你使用 *Value 还是 Value 并不重要。装箱的值仍然不会被复制。

这里的惯用方法是什么?我应该坚持返回副本的指针,还是尝试返回实际插入的原始元素?

如果 Value 是一个接口,那么我认为可以安全地说,总是按值传递它。如果你正在进行某种代码生成以获得伪泛型,那就很难说了。这真的取决于树将被使用的每个场景。有时,出于稳定性的考虑,你会需要副本,就像你提到的那样;但其他时候,也许你只是想要对树中某些嵌套字段或内容进行只读访问,那么如果你只是想从每个元素中提取一个字符串之类的东西,创建大型结构体的副本就是浪费的。

还要注意,树和链表一样,在现代硬件上非常慢。树中每一个引用不在缓存中的内存的指针解引用,都会使处理器停顿数百个周期。如果下一个节点需要解引用,并且那个内存不在缓存中,那么又会多出几百个周期,等等……大多数时候,在类似数组的数据结构中进行线性搜索会更快,除非你有数十万个元素,每个元素的大小达到数百或数千字节。如果你有一个简单的切片,那么用户知道要么:

for _, v := range vs {
    // 使用副本
}

或者

for i := range vs {
    v := &vs[i]
    // 使用指针
}

这取决于他们是否需要副本。

更多关于Golang数据结构设计:应该返回内部元素还是副本?语言特性强制返回副本!的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go中,返回内部元素还是副本取决于数据结构的语义和性能要求。对于树这种数据结构,通常有两种设计模式:

1. 返回副本(值语义)

func (t *tree) Largest() Value {
    // 找到最大节点
    var largest Value
    // ... 遍历逻辑
    return largest // 返回副本
}

2. 返回内部元素的引用(指针语义)

func (t *tree) Largest() *Value {
    // 找到最大节点
    var largest *Value
    // ... 遍历逻辑
    return largest // 返回内部指针
}

生产环境中的实际考虑:

示例1:返回副本(线程安全,但可能影响性能)

type Tree struct {
    mu    sync.RWMutex
    root  *node
}

func (t *Tree) Largest() Value {
    t.mu.RLock()
    defer t.mu.RUnlock()
    
    var largest Value
    // 遍历找到最大值
    // largest = ... 
    return largest // 值拷贝
}

示例2:返回指针(高性能,但需要文档说明)

// Largest 返回树中最大元素的指针。
// 注意:返回的是内部数据的引用,修改返回值会影响树的内容。
// 调用者不应修改返回的值,除非明确知道后果。
func (t *Tree) Largest() *Value {
    t.mu.RLock()
    defer t.mu.RUnlock()
    
    // 直接返回内部节点的指针
    return &t.findLargest().value
}

示例3:混合方案(提供两种选择)

func (t *Tree) Largest() Value {
    // 返回副本
    return t.largestNode().value
}

func (t *Tree) LargestRef() *Value {
    // 返回引用(需要文档明确说明)
    return &t.largestNode().value
}

func (t *Tree) LargestCopy() Value {
    // 明确表明返回副本
    val := t.largestNode().value
    return val
}

文档实践示例:

// Tree 是一个并发安全的二叉搜索树
type Tree struct {
    // ...
}

// Largest 返回树中最大元素的副本。
// 返回的是值的拷贝,修改返回值不会影响树中的原始数据。
func (t *Tree) Largest() Value

// LargestRef 返回指向树中最大元素的指针。
// 警告:返回的是内部数据的引用,修改返回值会直接影响树的内容。
// 此方法主要用于性能关键场景,调用者必须确保不修改返回的值。
func (t *Tree) LargestRef() *Value

性能对比示例:

// 基准测试显示差异
func BenchmarkLargestCopy(b *testing.B) {
    t := NewTree()
    // 填充大量数据
    for i := 0; i < b.N; i++ {
        _ = t.Largest() // 值拷贝
    }
}

func BenchmarkLargestRef(b *testing.B) {
    t := NewTree()
    // 填充大量数据
    for i := 0; i < b.N; i++ {
        _ = t.LargestRef() // 仅返回指针
    }
}

在标准库中,sync.MapLoad 方法返回的是值的副本,而 container/listFront() 返回的是内部元素的指针。选择哪种方式取决于你的具体需求:如果数据较小或需要线程安全,返回副本;如果数据较大且性能关键,返回指针但必须明确文档说明。

回到顶部