6

I've found myself using the following pattern as a way to get optional parameters with defaults in Go struct constructors:

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)
}

Is there a better way of allowing for optional parameters with defaults?

Robert Kajic
  • 8,689
  • 4
  • 44
  • 43
  • 1
    what is wrong with: http://play.golang.org/p/DYw5pWzRQC ? less code, better understandable, more universal... the above looks like a solution waiting for a problem... – metakeule Nov 22 '13 at 22:29
  • At the moment, I am using this pattern to create fixtures in tests. IMHO, assigning defaults explicitly would not make those tests more understandable or more readable. – Robert Kajic Nov 23 '13 at 12:08

5 Answers5

5

Dave Cheney offered a nice solution to this where you have functional options to overwrite defaults:

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

So your code would become:

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

Iain Duncan
  • 3,139
  • 2
  • 17
  • 28
3

The approach seems reasonable to me. However, you have a bug. If I explicitly set Type to 0, it will get switched to 1.

My suggested fix: Use a struct literal for the default value: http://play.golang.org/p/KDNUauy6Ie

Or perhaps extract it out: http://play.golang.org/p/QpY2Ymze3b

Tyler
  • 21,762
  • 11
  • 61
  • 90
  • But now the only way to get a default for Type is to not pass any object at all to NewObject. NewObject(&Object{Name: "foo"}) will return an object with Type 0. I think I'd rather not allow Type 0 then. If you need Type 0, you could set it after calling the constructor, on the returned object. – Robert Kajic Nov 22 '13 at 18:33
1

Take a look at "Allocation with new" in Effective Go. They explain about making zero-value structs a useful default.

If you can make Object.Type (and your other fields) have a default of zero, then Go struct literals already give you exactly the feature you're requesting.

From the section on composite literals:

The fields of a composite literal are laid out in order and must all be present. However, by labeling the elements explicitly as field:value pairs, the initializers can appear in any order, with the missing ones left as their respective zero values.

That means you can replace this:

obj1 := NewObject(&Object{Name: "foo"})
obj2 := NewObject(nil)
obj3 := NewObject(&Object{Type: 2, Name: "foo"})

with this:

obj1 := &Object{Name: "foo"}
obj2 := &Object{}
obj3 := &Object{Type: 2, Name: "foo"}

If it is not possible to make the zero value the default for all of your fields, the recommended approach is a constructor function. For example:

func NewObject(typ int, name string) *Object {
    return &Object{Type: typ, Name: name}
}

If you want Type to have a nonzero default, you can add another constructor function. Suppose Foo objects are the default and have Type 1.

func NewFooObject(name string) *Object {
    return &Object{Type: 1, Name: name}
}

You only need to make one constructor function for each set of nonzero defaults you use. You can always reduce that set by changing the semantics of some fields to have zero defaults.

Also, note that adding a new field to Object with a zero default value doesn't require any code changes above, because all struct literals use labeled initialization. That comes in handy down the line.

Sean
  • 1,785
  • 10
  • 15
  • It's sometimes possible and intuitive to make use of the zero values of struct fields, but not always and unfortunately not in my case. I think it makes sense to create specialized constructors, like your NewFooObject, when the number of fields that need non-zero defaults is small, but not when the number of fields is say greater than two, because the number of special constructors grows combinatorially. – Robert Kajic Nov 24 '13 at 18:01
1

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

Here's an alternative that uses a method of the object to set defaults. I've found it useful a few times, although it's not much different than what you have. This might allow better usage if it's part of a package. I don't claim to be a Go expert, maybe you'll have some extra input.

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)
}

Output:

{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}
akahunahi
  • 1,782
  • 23
  • 21
0

You could use the ... operator.

instead of writing ToCall(a=b) like in python you write, ToCall("a",b)

See the 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{})

}
fabrizioM
  • 46,639
  • 15
  • 102
  • 119
  • Note this is seen elsewhere, for example the gorilla web toolkit. – Chris Pfohl Nov 22 '13 at 18:22
  • This requires that ToCall does type assertions (which may fail at runtime) and significant modifications to ToCall to allow for default parameters. It's an interesting example of how one might fake keyword arguments, but I don't think it answers my question. – Robert Kajic Nov 24 '13 at 11:12