在Go语言的结构体构造函数中设置默认值的可选参数

6

我发现在Go语言结构体构造函数中,使用以下模式可以获取带有默认值的可选参数:

package main

import (
    "fmt"
)

type Object struct {
    Type int
    Name string
}

func NewObject(obj *Object) *Object {
    if obj == nil {
        obj = &Object{}
    }
    // Type has a default of 1
    if obj.Type == 0 {
        obj.Type = 1
    }
    return obj
}

func main() {
    // create object with Name="foo" and Type=1
    obj1 := NewObject(&Object{Name: "foo"})
    fmt.Println(obj1)

    // create object with Name="" and Type=1
    obj2 := NewObject(nil)
    fmt.Println(obj2)

    // create object with Name="bar" and Type=2
    obj3 := NewObject(&Object{Type: 2, Name: "foo"})
    fmt.Println(obj3)
}

有没有更好的方法允许带有默认值的可选参数?


1
什么地方有问题:http://play.golang.org/p/DYw5pWzRQC? 少写代码,更易懂,更加通用... 上述代码看起来像是在寻找一个问题的解决方案... - metakeule
目前,我正在使用这种模式在测试中创建固定装置。在我看来,明确分配默认值不会使这些测试更易于理解或更可读。 - Robert Kajic
5个回答

5

Dave Cheney提供了一个很好的解决方案,其中您可以使用函数选项来覆盖默认设置:

https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

这样,你的代码会变成:

package main

import (
    "fmt"
)

type Object struct {
    Type int
    Name string
}

func NewObject(options ...func(*Object)) *Object {
    // Setup object with defaults 
    obj := &Object{Type: 1}
    // Apply options if there are any
    for _, option := range options {
        option(obj)
    }
    return obj
}

func WithName(name string) func(*Object) {
    return func(obj *Object) {
        obj.Name = name
    }
}

func WithType(newType int) func(*Object) {
    return func(obj *Object) {
        obj.Type = newType
    }
}

func main() {
    // create object with Name="foo" and Type=1
    obj1 := NewObject(WithName("foo"))
    fmt.Println(obj1)

    // create object with Name="" and Type=1
    obj2 := NewObject()
    fmt.Println(obj2)

    // create object with Name="bar" and Type=2
    obj3 := NewObject(WithType(2), WithName("foo"))
    fmt.Println(obj3)
}

https://play.golang.org/p/pGi90d1eI52


3

但现在获取Type的默认值的唯一方法是根本不传递任何对象到NewObject。NewObject(&Object {Name:“foo”})将返回一个Type为0的对象。我想我宁愿不允许Type为0。如果您需要Type 0,则可以在调用构造函数后,在返回的对象上设置它。 - Robert Kajic

1

https://play.golang.org/p/SABkY9dbCOD

这里有一种替代方法,使用对象的一种方法来设置默认值。我发现它在一些情况下很有用,尽管它与你所拥有的并没有太大区别。如果它是一个包的一部分,这可能会使其更好地使用。我不自称是Go专家,也许你会有额外的输入。
package main

import (
    "fmt"
)

type defaultObj struct {
    Name      string
    Zipcode   int
    Longitude float64
}

func (obj *defaultObj) populateObjDefaults() {
    if obj.Name == "" {
        obj.Name = "Named Default"
    }
    if obj.Zipcode == 0 {
        obj.Zipcode = 12345
    }
    if obj.Longitude == 0 {
        obj.Longitude = 987654321
    }
}

func main() {
    testdef := defaultObj{Name: "Mr. Fred"}
    testdef.populateObjDefaults()
    fmt.Println(testdef)

    testdef2 := defaultObj{Zipcode: 90210}
    testdef2.populateObjDefaults()
    fmt.Println(testdef2)

    testdef2.Name = "Mrs. Fred"
    fmt.Println(testdef2)

    testdef3 := defaultObj{}
    fmt.Println(testdef3)
    testdef3.populateObjDefaults()
    fmt.Println(testdef3)
}

输出:

{Mr. Fred 12345 9.87654321e+08}
{Named Default 90210 9.87654321e+08}
{Mrs. Fred 90210 9.87654321e+08}
{ 0 0}
{Named Default 12345 9.87654321e+08}

1
请看Effective Go中有关使用new分配内存的文章。它们解释了如何使零值结构体成为有用的默认值。
如果您可以让Object.Type(以及其他字段)具有零值默认值,那么Go结构字面量已经给您提供了您所请求的功能。
复合字面量的部分来看:

复合字面量的字段按顺序排列,并且必须全部存在。但是,通过将元素明确标记为field:value对,初始化器可以以任何顺序出现,缺少的元素留为空值。

这意味着您可以替换为:
obj1 := NewObject(&Object{Name: "foo"})
obj2 := NewObject(nil)
obj3 := NewObject(&Object{Type: 2, Name: "foo"})

使用这个:
obj1 := &Object{Name: "foo"}
obj2 := &Object{}
obj3 := &Object{Type: 2, Name: "foo"}

如果无法将零值设置为所有字段的默认值,则建议的方法是使用构造函数。例如:
func NewObject(typ int, name string) *Object {
    return &Object{Type: typ, Name: name}
}

如果你希望Type有一个非零的默认值,你可以添加另一个构造函数。假设Foo对象是默认值,并且具有Type 1。
func NewFooObject(name string) *Object {
    return &Object{Type: 1, Name: name}
}

你只需要为每组非零默认值编写一个构造函数即可。通过将某些字段的语义更改为具有零默认值,您始终可以减少该集合。
此外,请注意,向Object添加一个具有零默认值的新字段不需要进行任何代码更改,因为所有结构字面量都使用标签初始化。这在以后会派上用场。

有时候利用结构体字段的零值是可能且直观的,但不总是可行的,也很遗憾不适用于我的情况。我认为当需要非零默认值的字段数量较少时,像 NewFooObject 这样创建专门的构造函数是有意义的,但当字段数量超过两个时,它就不再适用了,因为特殊构造函数的数量会呈组合增长。 - Robert Kajic

0
你可以使用...操作符。
与在Python中写ToCall(a=b)不同,你可以写成ToCall("a", b)。
请查看Go Play Example
func GetKwds(kwds []interface{}) map[string]interface{} {
    result := make(map[string]interface{})

    for i := 0; i < len(kwds); i += 2 {
        result[kwds[i].(string)] = kwds[i+1]
    }

    return result
}

func ToCall(kwds ...interface{}) {
    args := GetKwds(kwds)
    if value, ok := args["key"]; ok {
        fmt.Printf("key: %#v\n", value)
    }
    if value, ok := args["other"]; ok {
        fmt.Printf("other: %#v\n", value)
    }
}

func main() {
    ToCall()
    ToCall("other", &map[string]string{})
    ToCall("key", "Test", "other", &Object{})

}

请注意,这在其他地方也可以看到,例如大猩猩 Web 工具包。 - Chris Pfohl
这需要ToCall进行类型断言(可能在运行时失败)并对ToCall进行重大修改,以允许默认参数。这是一个有趣的例子,说明了如何模拟关键字参数,但我不认为它回答了我的问题。 - Robert Kajic

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