【问题标题】:Optional parameters with defaults in Go struct constructorsGo struct 构造函数中具有默认值的可选参数
【发布时间】:2013-11-22 17:13:27
【问题描述】:

我发现自己使用以下模式来获取 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)
}

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

【问题讨论】:

  • 怎么了:play.golang.org/p/DYw5pWzRQC?更少的代码,更好的理解,更通用的......上面看起来像是一个等待问题的解决方案......
  • 目前,我正在使用这种模式在测试中创建夹具。恕我直言,明确分配默认值不会使这些测试更易于理解或更具可读性。

标签: constructor go default


【解决方案1】:

这种方法对我来说似乎是合理的。但是,您有一个错误。如果我将Type 显式设置为0,它将get switched to 1

我建议的解决方法:使用结构文字作为默认值:http://play.golang.org/p/KDNUauy6Ie

或者也许将其提取出来:http://play.golang.org/p/QpY2Ymze3b

【讨论】:

  • 但是现在获得 Type 的默认值的唯一方法是根本不将任何对象传递给 NewObject。 NewObject(&Object{Name: "foo"}) 将返回一个类型为 0 的对象。我想我宁愿不允许类型 0。如果需要Type 0,可以在调用构造函数后,在返回的对象上设置。
【解决方案2】:

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】:

    看看"Allocation with new" in Effective Go。他们解释了如何使零值结构成为有用的默认值。

    如果您可以使 Object.Type(和您的其他字段)的默认值为零,那么 Go 结构文字已经为您提供了您所要求的功能。

    来自composite literals的部分:

    复合文字的字段按顺序排列,并且必须全部存在。但是,通过将元素显式标记为 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"}
    

    如果无法将零值设为所有字段的默认值,recommended approach is a constructor function。例如:

    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,当需要非零默认值的字段数量很少时,但当字段数量大于两个时,因为特殊构造函数的数量会组合增长。
    【解决方案4】:

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

    这是一个替代方法,它使用对象的方法来设置默认值。我发现它有用几次,尽管它与您所拥有的并没有太大不同。如果它是包的一部分,这可能允许更好的使用。我不自称是围棋专家,也许你会有一些额外的意见。

    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}
    

    【讨论】:

      【解决方案5】:

      您可以使用 ... 运算符。

      ToCall(a=b) 不像你在 python 中那样写,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{})
      
      }
      

      【讨论】:

      • 请注意,这在其他地方可以看到,例如 gorilla web 工具包。
      • 这要求 ToCall 确实键入断言(可能在运行时失败)并对 ToCall 进行重大修改以允许默认参数。这是一个有趣的例子,说明人们如何伪造关键字参数,但我认为它不能回答我的问题。
      猜你喜欢
      • 2017-09-27
      • 1970-01-01
      • 2017-11-21
      • 2016-09-05
      • 1970-01-01
      • 2015-01-28
      • 1970-01-01
      • 2020-10-15
      • 1970-01-01
      相关资源
      最近更新 更多