复制包含指针的结构体的 Golang 实现

7

TL;DR:如何在golang中创建包含指针的struct,并安全地将其按值传递给其他函数?(安全是指无需担心这些函数可以取消引用所说的指针并更改它所指向的变量)。如果要给出的答案是“复制函数”,那么如何删除原始复制构造函数/运算符?使用自定义复制函数覆盖它?或以其他方式阻止人们使用它?

在golang中,我可以拥有一个结构体,其中包含一个指向动态分配变量的指针。

我也可以将这些结构的实例传递给“复制”它们的函数。

然而,我不能覆盖或删除内置的复制运算符。这意味着,理论上,我可以有以下代码:

import (
        "fmt"
)

type A struct {
        a * int
}

func main() {
        var instance A
        value := 14
        instance.a = &value
        fmt.Println(*instance.a) // prints 14
        mutator(instance)
        fmt.Println(*instance.a) // prints 11 o.o
}

func mutator(instance A) {
        *instance.a = 11
        fmt.Println(*instance.a)
}

这种代码显然在这里有点无意义。然而,假设成员字段“a”是一个复杂的结构,那么访问它的函数可能会尝试修改它,这也是可以理解的。
调用函数"mutator"后,程序员可能希望继续使用他的A实例,并且(假设他并没有编写结构或了解其内部情况)甚至可能认为,由于他传递的是副本而不是指针,他的A实例将保持不变。
现在,除了golang之外,还有几种(3种)流行的语言允许程序员考虑分配和操作内存。我不了解Rust或C,因此我将按照自己的方式来解决C++中出现的问题:
a)假设我是类A的设计者,我可以建立一个拷贝构造函数,从而得到以下代码:
#include <iostream>

    class A {
    public:
            int * a;
            A(int value): a(new int{value}) {}
            A(const A & copyFrom): a(new int{*copyFrom.a}) {}
    };

    void mutator(A instance) {
            *instance.a = 11;
            std::cout << *instance.a << "\n";
    }


    int main() {
            A instance{14};
            std::cout << *(instance.a) << "\n";
            mutator(instance);
            std::cout << *instance.a << "\n";
    }

这使得我的类的实例可以被复制,同时指针也将被重新分配。

b) 假设我是类A的设计者,并且不想构建复制构造函数(假设a指向的任何内容都可能非常大,或者A经常在性能关键的条件下作为只读对象使用),但仍希望确保对复制进行的任何赋值都不能修改a指向的值(但仍允许通过将其赋值给新值来修改a),我可以这样编写我的类:

class A {
public:
        const int * a;
        A(int value): a(new const int{value}) {}
};

以下代码将无法通过编译:
void mutator(A instance) {
        *instance.a = 11;
        std::cout << *instance.a << "\n";
}


int main() {
        A instance{14};
        std::cout << *(instance.a) << "\n";
        mutator(instance);
        std::cout << *instance.a << "\n";
}

但是下面的代码可以编译成功:
void mutator(A instance) {
        instance.a = new const int{11};
        std::cout << *instance.a << "\n";
}


int main() {
        A instance{14};
        std::cout << *(instance.a) << "\n";
        mutator(instance);
        std::cout << *instance.a << "\n";
}

请注意,这是C++的“面向对象”(呕吐)设计的典型例子。我认为如果函数签名中有某种规则可以保证不修改传递给它的A实例或声明A实例为“const”并针对其动态分配的字段(而不仅是静态字段)进行“保护”,那将更好。

然而,虽然这个解决方案可能不完美,但它是一个解决方案。它使我对我的A实例的“所有权”有了清晰的想法。

在golang中,似乎任何包含指针的实例的“副本”基本上都是自由的,即使结构体的作者有这样的意图,也无法安全地传递它。

我唯一能想到的方法是编写一个“Copy”方法,返回该结构的全新实例(类似于上面示例中的复制构造函数)。但是,没有删除复制构造函数/运算符的能力,很难确保人们会使用和/或注意到它。

说实话,我觉得很奇怪,在golang中甚至允许重新编写指针的内存地址而不使用“unsafe”包或类似的东西。

是否禁止此类操作会更合理,就像许多其他操作一样?

考虑到“append”的工作方式,似乎作者的意图是支持将新变量重新分配给指针,而不是对之前指向的变量进行突变。但是,对于自定义结构(至少没有在包中封装该结构),要强制执行这一点似乎很难实现,对于像切片或数组这样的内置结构则很容易。

我是否忽略了在golang中进行复制构造(或禁止复制)的方法?是否确实是作者的原始意图在时间和内存允许的情况下鼓励重新赋值而不是突变?如果是这样的话,为什么动态分配的变量变异如此容易?有没有一种方法可以模拟结构体或文件的私有/公共行为而不是完整的包?有没有其他方法可以强制执行具有指针的结构的某种形式的所有权,我可能忽略了?


请编辑问题,将其限制为具有足够细节以确定充分答案的特定问题。避免一次性提出多个不同的问题。 - peterSO
在Go中,这非常简单:使用您的包的人无法访问未公开的标识符。也就是说,从C++的角度来看,它们实际上是私有的。如果您提供了一种访问结构体中指针的方法(例如返回这样一个指针的方法),那么您可以复制其数据并返回指向该副本的指针,或者仅信任您的包的用户不会破坏事物。双方都有论据,但最终取决于您的用例。 - user539810
1个回答

2
如何在golang中创建一个包含指针的结构体,然后安全地按值传递给其他函数?(安全地意味着不必担心这些函数可以取消引用该指针并更改其指向的变量)。
使用具有未导出字段的导出包类型。例如,
src / ptrstruct / ptrstruct.go:
package ptrstruct

type PtrStruct struct {
    pn *int
}

func New(n int) *PtrStruct {
    return &PtrStruct{pn: &n}
}

func (s *PtrStruct) N() int {
    return *s.pn
}

func (s *PtrStruct) SetN(n int) {
    *s.pn = n
}

func (s *PtrStruct) Clone() *PtrStruct {
    // make a deep clone
    t := &PtrStruct{pn: new(int)}
    *t.pn = *s.pn
    return t
}

src/ptrstruct.go:

package main

import (
    "fmt"

    "ptrstruct"
)

func main() {
    ps := ptrstruct.New(42)
    fmt.Println(ps.N())
    pc := ps.Clone()
    fmt.Println(pc.N())
    pc.SetN(7)
    fmt.Println(pc.N())
    fmt.Println(ps.N())
}

输出:

src $ go run ptrstruct.go
42
42
7
42
src $ 

如果你要回答的是“拷贝函数”,那么我该如何删除原始的拷贝构造函数/操作符?用自定义的拷贝函数来覆盖它吗?还是以其他方式阻止人们使用它?
停止在C++中编程,开始在Go中编程。通过设计,Go不是C++。
“拷贝构造函数/操作符”和“用自定义函数覆盖它”都是C ++的概念。
参考资料: 《Go编程语言规范》 声明和作用域 导出标识符

这个解决方案有几个问题。a)这意味着我必须在一个单独的包中声明结构体。b)我依赖于用户知道他不应该通过值传递结构体,而是使用复制/克隆方法...我希望有一种方法来覆盖或至少删除默认的复制构造函数...但似乎没有这样的方法,所以如果1-2天内没有更好的答案,我将把它标记为答案。 - George
另外,很抱歉使用了C++的概念,但我并不真正了解Go甚至是C的语义,最终,C++拥有Go所具备的每个概念,因此将Go的功能映射到C++非常容易,即使GO可能对于复制构造函数/操作有不同的名称...它仍然是一种在语言中存在的概念,我认为将两者关联起来也足够容易。 - George

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接