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语言学习——结构体构造函数方法和接收者给自定义类型添加方法
5.3 Go语言中构造函数与复合声明(Constructors and composite literals)