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

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

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

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

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

【问题讨论】:

标签: oop constructor go


【解决方案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 &Thing{someParameter, 33}
}

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

func makeThing(name string) Thing {
    return Thing{name, 33}
}

参考:Allocation with new in Effective Go.

【讨论】:

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

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

  1. 将结构的零值设为合理的默认值。 (虽然这对于大多数来自“传统” oop 的人来说看起来很奇怪,但它通常可以工作并且非常方便)。
  2. 提供一个函数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 &Thing{someParameter, 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 &Logger{out: 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 &Color{R: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 &Color{R: 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 中工作。

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

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

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

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

【讨论】:

    【解决方案11】:

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

    你不能返回 nil。

    cannot use nil as type XYZ in return argument
    

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

    【讨论】:

    • 这是评论还是新问题?这似乎不是一个答案。
    • “咳咳”这个词是什么?你口述的时候咳嗽了吗?
    猜你喜欢
    • 1970-01-01
    • 2019-10-04
    • 1970-01-01
    • 2015-11-19
    • 2012-06-30
    • 1970-01-01
    • 2014-05-09
    • 2013-08-26
    • 1970-01-01
    相关资源
    最近更新 更多