Golang中的不可变结构体

55

在Golang中,是否可以定义不可变的结构体?一旦初始化,则仅对结构体字段进行读取操作,不能修改字段值。如果可以,如何实现。


这对我来说也是一篇很好的相关阅读:https://flaviocopes.com/go-copying-structs - David
4个回答

72

通过将结构体的成员设为非导出并提供读取器,可以使其在包外部只读。例如:

package mypackage

type myReadOnly struct {
  value int
}

func (s myReadOnly) Value() int {
  return s.value
}

func NewMyReadonly(value int) myReadOnly{
  return myReadOnly{value: value}
}

使用方法:

myReadonly := mypackage.NewMyReadonly(3)
fmt.Println(myReadonly.Value())  // Prints 3

这很有帮助。在包内部将结构体设置为只读是否可行? - Vikram Singh Choudhary
6
不需要,因为你可以控制包内的代码,所以只需在结构体创建后不再写入即可。 - Milo Christiansen
9
@VIKRAMSINGHCHOUDHARY,尽管这可能有些过度,但你也可以将你的类型移入自己的包中,在那里只有它、它导出的方法和构造函数。请注意不改变原意,并使内容更加通俗易懂。 - mkopriva
1
就像@mkopriva所说的那样 - 利用Go的内部包来防止其他人使用(和依赖)此软件包。 - colm.anseo
我正在制作一个 struct 来模拟 toml 配置文件。要解析 TOML,我们需要导出字段。另一方面,我希望仅在应用程序启动时创建配置结构,因此将其设置为只读对我有意义。我似乎找不到同时解决这两个问题的解决方案。 - Abhijit
显示剩余2条评论

12

无法以通用的方式标记字段/变量为只读。您能做的唯一事情是将字段/变量标记为未导出(首字母小写),并提供公共的getter来防止其他包编辑变量。


然而,这并不能阻止例如另一个团队成员在同一个包上工作时无意中更改字段的情况。我真的不明白为什么GO语言设计者很难允许const或readonly构造。我希望GO不会走向Python的方式。 - Lord of Scripts

5
在Go语言中,无法定义不可变结构:结构体字段是可变的,const关键字不适用于它们。然而,通过简单的赋值操作,Go很容易地复制整个结构体,因此我们可能认为通过按值传递参数就可以实现不可变性,以复制为代价。
然而,毫不奇怪,这并不会复制指针引用的值。由于内置的集合(map、slice和array)都是引用且可变的,所以复制包含其中之一的结构体只会复制指向相同底层内存的指针。
示例:
type S struct {
    A string
    B []string
}

func main() {
    x := S{"x-A", []string{"x-B"}}
    y := x // copy the struct
    y.A = "y-A"
    y.B[0] = "y-B"

    fmt.Println(x, y)
    // Outputs "{x-A [y-B]} {y-A [y-B]}" -- x was modified!
}

注意:因此,您必须非常小心,并且不要假定通过值传递参数会使其不可变。

有一些深拷贝库尝试通过(慢速)反射来解决这个问题,但它们效果不佳,因为私有字段无法使用反射访问。因此,为了避免竞态条件而进行防御性复制将是困难的,需要大量样板代码。甚至没有一个克隆接口可以标准化此过程。

来源:https://bluxte.net/


7
2020年对我来说感觉很疯狂。在 Golang 中缺乏不可变性是否是一个热门话题,还是系统人员不关心的事情? - Anthony Naddeo
2
我不同意“系统人员”的说法。例如,在Rust中,不可变是默认的。而且,没错,Golang不支持这一点很疯狂。 - Frank Schmitt
2
@AnthonyNaddeo Go语言最初的设计目标之一是:保持语言轻量简单,易于阅读和快速编译,并通过轻量级例程和通信通道实现并发,而不是共享内存。不变性使共享内存变得廉价和安全,但它增加了编译器的复杂性。由于您不应该共享内存,编译器应该快速,因此最初包括显式的不可变性(或默认的不可变性和选择性的可变性,如Rust所做的那样)并不是优先考虑的。 - kbolino
切片和映射内部有指针,但数组没有。在 Go 中,一个数组(例如 [3]float32[10]int,请注意明确的大小)是一个值,并且像结构体一样进行复制。实际上,[3]intstruct{x,y,z int} 具有相同的内存布局,与 []int 完全不同。 - kbolino

-3
如果您使用Golang编写一个函数式结构体,那么它必须是一个不可变的结构体,例如您可以编写一个明确的结构体。
type Maybe[T any] struct {
    v     T
    valid bool
}

func (m Maybe[T]) Just() T {
    return m.v
}

func (m Maybe[T]) Nothing() bool {
    return m.valid == false
}

func Just[T any](v T) Maybe[T] {
    return Maybe[T]{
        v:     v,
        valid: true,
    }
}

func Nothing[T any]() Maybe[T] {
    return Maybe[T]{
        valid: false,
    }
}

maybe 结构体是一个不可变的结构体


1
这个回答似乎没有为现有的回答增加太多内容。您介意添加更多细节吗?例如,您可以解释一下代码中涉及的Go 1.18语法。 - Chang Qian
1
我认为很明显,maybe struct 是一个不可变的结构体,你还想了解更多细节吗? - ych
2
这不是一个不可变的结构体。该结构体的字段可以在其自己的包内进行更改。由于它们没有被导出,其他包中的代码无法直接访问它们,因此如果在它们的包中没有提供任何修改器函数,则无法对它们进行更改。因此,这个答案基本上与 https://dev59.com/plYN5IYBdhLWcg3wT2ta#47633820 相同 - 只是缺少了清晰的解释。 - Simon Kissane

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