Go 中的构造函数

Posted

技术标签:

【中文标题】Go 中的构造函数【英文标题】:Constructors in Go 【发布时间】:2013-08-10 03:24:55 【问题描述】:

我有一个结构,我希望用一些合理的默认值对其进行初始化。

通常,这里要做的是使用构造函数,但由于 Go 在传统意义上并不是真正的 OOP,因此这些不是真正的对象,并且它没有构造函数。

我注意到了 init 方法,但那是在包级别。还有其他类似的东西可以在结构级别使用吗?

如果不是,那么 Go 中此类事物的公认最佳实践是什么?

【问题讨论】:

尝试在github上使用这个库:github.com/mobiletoly/gobetter我这样做是为了生成必填字段并提供现成的构造函数 【参考方案1】:

当零值不能产生合理的默认值或结构初始化需要某些参数时,有一些构造函数的等价物。

假设你有一个这样的结构:

type Thing struct 
    Name  string
    Num   int

那么,如果零值不合适,您通常会使用返回指针的NewThing 函数构造一个实例:

func NewThing(someParameter string) *Thing 
    p := new(Thing)
    p.Name = someParameter
    p.Num = 33 // <- a very sensible default value
    return p

当你的结构足够简单时,你可以使用这个压缩结构:

func NewThing(someParameter string) *Thing 
    return &ThingsomeParameter, 33

如果不想返回指针,那么一种做法是调用函数makeThing而不是NewThing

func makeThing(name string) Thing 
    return Thingname, 33

参考:Allocation with new in Effective Go.

【讨论】:

好的,这是有道理的,但这意味着这些客户必须了解新功能并制作功能。即这不是所有结构的标准。我想这可以用接口来处理 我不确定你的意思。拥有 NewThing 功能是标准的。如果您的意思是它们不会被自动调用,是的,但是无论如何您都不能自动使用结构。我认为你不应该尝试隐藏那些带有接口的构造函数,它们出现时代码更清晰。 使用new 分配结构并在之后设置值并不常见。结构文字是那里的首选方式。而且我也不确定您的“makeThing”命名约定。标准库一致地调用构造函数 New() 或 NewThing(),我自己从未遇到过任何 makeThing() 函数...... 是的,但是下面的“Effective Go”段落介绍了结构字面量,并演示了如何使用它们来编写NewFile 构造函数的更惯用版本:) 我忍不住,但这违背了封装的原则。在一个包含多个“类”的数据包中(无论你如何安排它,这就是像结构 + 方法这样的概念),人们不应该再处理命名冲突了。但是在这种荣耀的 go 方法中,您现在基本上不知道哪个函数将构造函数角色作为工厂。所以你最终会得到像“NewClass”这样的命名约定。 Go 是一种糟糕的编码风格。【参考方案2】:

实际上有两种公认的最佳做法:

    将结构的零值设为合理的默认值。 (虽然这对于大多数来自“传统” oop 的人来说看起来很奇怪,但它通常可以工作并且非常方便)。 提供一个函数func New() YourTyp,或者如果你的包函数中有几个这样的类型func NewYourType1() YourType1等等。

记录您的类型的零值是否可用(在这种情况下,它必须由New... 函数之一设置。(对于“传统主义者”哎呀:不阅读文档的人获胜'不能正确使用你的类型,即使他不能在未定义的状态下创建对象。)

【讨论】:

这对地图等属性有何作用。这个的默认值是 nil,对吧?因此,这些是否应该始终通过 New 函数进行初始化? 是与否,这取决于。很可能是的,提供func New() T。但根据具体情况,您可以仅在需要时检查此 nil 映射和 make 一个。在这种情况下:记录此映射创建是否可以安全地并发使用(也就是使映射受到保护的代码,例如通过互斥锁。)。取决于地图是否导出......很难不看代码就知道。【参考方案3】:

Go 有对象。对象可以有构造函数(虽然不是自动构造函数)。最后,Go 是一种 OOP 语言(数据类型附加了方法,但不可否认的是,OOP 是什么有无穷无尽的定义。)

尽管如此,公认的最佳实践是为您的类型编写零个或多个构造函数。

@dystroy 在我完成这个答案之前发布了他的答案,让我添加他的示例构造函数的替代版本,我可能会写成:

func NewThing(someParameter string) *Thing 
    return &ThingsomeParameter, 33 // <- 33: a very sensible default value

我想向您展示这个版本的原因是,通常可以使用“内联”文字而不是“构造函数”调用。

a := NewThing("foo")
b := &Thing"foo", 33

现在*a == *b

【讨论】:

+1 因为平等。这可能有点(或完全)离题,但很重要。我认为它是 Go1 附带的,不是吗? 您能否进一步解释为什么 a 和 b 相等? @lazywei a != b, 但是 *a == *b 因为它们指向的结构具有相同的字段,请参阅play.golang.org/p/A3ed7wNVVA 示例 @lazywei 它检查浅层相等还是深层相等? (例如,如果Thing 包含一个映射,则最好检查深度相等性。)【参考方案4】:

Golang 在其官方文档中并不是 OOP 语言。 Golang struct 的所有字段都有一个确定的值(不像 c/c++),因此构造函数不像 cpp 那样必要。 如果您需要为某些字段分配一些特殊值,请使用工厂函数。 Golang 的社区建议使用新的.. 模式名称。

【讨论】:

如果你的结构字段需要先初始化,你可能需要构造函数 @DevX new / factory 模式是一个不错的选择。【参考方案5】:

Go 中没有默认构造函数,但您可以为任何类型声明方法。您可以养成声明一个名为“Init”的方法的习惯。不确定这是否与最佳实践相关,但它有助于保持名称简短而不会失去清晰度。

package main

import "fmt"

type Thing struct 
    Name string
    Num int


func (t *Thing) Init(name string, num int) 
    t.Name = name
    t.Num = num


func main() 
    t := new(Thing)
    t.Init("Hello", 5)
    fmt.Printf("%s: %d\n", t.Name, t.Num)

结果是:

Hello: 5

【讨论】:

问题:语义上,t := new(Thing) \n t.Init(...)var t Thing \n t.Init(...) 相同,对吧?哪种形式在 Go 中更惯用?【参考方案6】:

另一种方法是;

package person

type Person struct 
    Name string
    Old  int


func New(name string, old int) *Person 
    // set only specific field value with field key
    return &Person
        Name: name,
    

【讨论】:

不应该是 NewPerson 而不是 New 吗? @DevX 否,因为这是包的主要(可能只有)类型。您可以将其用作person.New(name, old)。与 person.NewPerson(name, old) 比较,会卡顿。 @FilipHaglund 但函数 New 不是 Person Struct 的方法,所以你不能调用 person.New @FilipHaglund 我同意这一点:“不,因为这是包的主要(可能只是)类型。” person 不是变量,而是包 :) 所以 New 是一个函数,而不是一个方法。【参考方案7】:

我喜欢这个blog post的解释:

函数 New 是用于创建核心类型或不同类型以供应用程序开发人员使用的包的 Go 约定。看看在 log.go、bufio.go 和 cypto.go 中 New 是如何定义和实现的:

log.go

// New creates a new Logger. The out variable sets the
// destination to which log data will be written.
// The prefix appears at the beginning of each generated log line.
// The flag argument defines the logging properties.
func New(out io.Writer, prefix string, flag int) * Logger 
    return &Loggerout: out, prefix: prefix, flag: flag

bufio.go

// NewReader returns a new Reader whose buffer has the default size.
func NewReader(rd io.Reader) * Reader 
    return NewReaderSize(rd, defaultBufSize)

crypto.go

// New returns a new hash.Hash calculating the given hash function. New panics
// if the hash function is not linked into the binary.
func (h Hash) New() hash.Hash 
    if h > 0 && h < maxHash 
        f := hashes[h]
        if f != nil 
            return f()
        
    
    panic("crypto: requested hash function is unavailable")

由于每个包都充当命名空间,因此每个包都可以有自己的 New 版本。在 bufio.go 中可以创建多种类型,因此没有独立的 New 功能。在这里你会发现像 NewReader 和 NewWriter 这样的函数。

【讨论】:

log 和 bufio 示例似乎是返回指针的函数,而 crypto Hash 似乎是一种构造方法,更像您在 Java 等其他 OOP 语言中所期望的。 Hash New() 方法也不返回指针,它返回一个新的 Hash。从这个意义上说,它看起来更像是一个工厂而不是初始化器。我只是想知道这一点,因为使用具有任何复杂性的 new 函数会使嵌入类型失去与其构造函数的联系,或者如果您希望伪继承成为可能,则迫使您重新实现它维护。【参考方案8】:

如果您想强制使用工厂函数,请将您的结构(您的类)命名为第一个字符小写。那么就不能直接实例化结构体,需要工厂方法。

这种基于第一个字符小写/大写的可见性也适用于结构字段和函数/方法。如果您不想允许外部访问,请使用小写。

【讨论】:

您应该将其留给使用您的类型的开发人员。将需要在包外使用的类型设为私有,并且只提供工厂可能会相当不方便。惯例是在可用的情况下使用工厂,否则您可能知道自己在做什么。 @dynom,我明白你的意思。但是,开发人员实例化我的结构并忘记(或不知道)调用构造函数是否存在巨大风险?因此,我接收到这样一个结构的每个方法都必须检查以确保实例已初始化。 如果开发人员选择了您的类型而不是您提供的工厂,则由他们来处理后果。你无法想象一个人可能会对你的代码做什么,所以不要尝试。假设使用您的代码的人足够聪明,可以做出这个决定。特别是在编写测​​试时,您可以只存根/模拟所需的内容,这非常令人愉快。你不应该把它从人们身上拿走。【参考方案9】:

在 Go 中,可以使用返回指向已修改结构的指针的函数来实现构造函数。

type Colors struct 
    R   byte
    G   byte
    B   byte


// Constructor
func NewColors (r, g, b byte) *Colors 
    return &ColorR:r, G:g, B:b

为了弱依赖和更好的抽象,构造函数返回的不是指向结构的指针,而是该结构实现的接口。

type Painter interface 
    paintMethod1() byte
    paintMethod2(byte) byte


type Colors struct 
    R byte
    G byte
    B byte


// Constructor return intreface
func NewColors(r, g, b byte) Painter 
    return &ColorR: r, G: g, B: b


func (c *Colors) paintMethod1() byte 
    return c.R


func (c *Colors) paintMethod2(b byte) byte 
    return c.B = b

【讨论】:

我不认为返回接口是最佳实践。您通常希望接受一个接口并返回一个指向结构的指针(可能实现一个接口)。它仍然是可测试的。调用代码必须将返回值视为接口类型。这种方式可以透明地分配给它。 重新调整界面并没有让 Mocking 变得更容易,只有接受才可以。你模拟你给实现的东西,你不需要模拟你得到的任何回报。因此,该语句不仅不正确,而且返回接口也是一种不好的做法。【参考方案10】:

我是新来的。我有一个来自其他语言的模式,它有构造函数。并且会在 go 中工作。

    创建一个init 方法。 使init 方法成为(对象)一次例程。它仅在第一次被调用时运行(每个对象)。
func (d *my_struct) Init ()
    //once
    if !d.is_inited 
        d.is_inited = true
        d.value1 = 7
        d.value2 = 6
    

    在此类的每个方法的顶部调用 init。

当您需要后期初始化(构造函数太早)时,此模式也很有用。

优点:隐藏了类中的所有复杂性,客户端不需要做任何事情。

缺点:你必须记得在类的每个方法的顶部调用Init

【讨论】:

【参考方案11】:

如果 New 函数失败了怎么办?

你不能返回 nil。

cannot use nil as type XYZ in return argument

由于 go 通过引用传递对象(并且我假设返回它们),所以没有意义,咳咳,返回一个指针。

【讨论】:

这是评论还是新问题?这似乎不是一个答案。 “咳咳”这个词是什么?你口述的时候咳嗽了吗?

以上是关于Go 中的构造函数的主要内容,如果未能解决你的问题,请参考以下文章

Go语言自学系列 | golang构造函数

Go语言学习——结构体构造函数方法和接收者给自定义类型添加方法

5.3 Go语言中构造函数与复合声明(Constructors and composite literals)

5.3 Go语言中构造函数与复合声明(Constructors and composite literals)

用Go实现Java中构造函数SetGettoString

用Go实现Java中构造函数SetGettoString