'How to deal with long parameter lists in `New(...` functions

Say I have a localised struct called MyStruct with the following body:

struct MyStruct {
  myField1 string
  myField2 string
  myField3 string
  ...
  myFieldN string
}

And a function which instantiates new MyStructs for external callers:

func NewMyStruct(myField1, myField2, myField3, ..., myFieldN string) MyStruct {
  return MyStruct{
    myField1: myField1,
    myField2: myField2,
    myField3: myField3,
    ...
    myFieldN: myFieldN,
  }
}

Question: How would I best deal with the scenario of there being just too many fields within the struct resulting in a NewMyStruct(... function with way too many parameters? Is there any best practice to mitigate this issue? As of now, I have several functions like this in my codebase:

func NewSuperStruct(myField1, myField2, myField3, myField4, myField5, myField6, myField7, myField8, myField9, myField10, myField11, myField12, myField13, myField14, myField15, myField16, myField17, myField18, myField19, myField20, myField21, myField22) ...

But it's not necessarily that the structs themselves are nonsensical in the sense that the properties/fields don't belong within, in my application they do make sense, the structs are just too large, that's all.

go


Solution 1:[1]

I'd say just don't have the New func:

struct MyStruct {
  myField1 string
  myField2 string
  myField3 string
}

val := MyStruct{
    myField1: "one",
    myField2: "two",
    myField3: "three",
}

If unexported fields need to be set from another package, use some kind of options or config:

type MyStruct struct {
    Exported   string
    unexported string
}

type MyStructOptions struct {
    Exported   string
    Unexported string
}

func NewMyStruct(opts MyStructOptions) *MyStruct {
    return &MyStruct{
        Exported: opts.Exported,
        unexported: opts.Unexported,
    }
}

Solution 2:[2]

Personally (obviously depending on the goal of the struct) I am a big fan of functional options:

type MyStructOpts func(*MyStruct)

func WithField1(field1 string) MyStructOps {
  return func(m *MyStruct) {
    m.myField1 = field1
  }
}

func New(opts ...MyStructOpts) *MyStruct {
  m := MyStruct{
    myField1: "someDefaultIfneeded",
  }

  for _, opt := range opts {
    opt(&m)
  }

  return &m
}

which can be used as follows:

New(
  WithField1("someString"),
  ...
)

This has a couple of benefits:

  • Callers of new do not need to worry about the order
  • Passing values is explicit with field name, which means you won't mix up Field1 & Field2
  • You have the ability to pass different defaults to MyStruct in case callers don't pass WithField1
  • Adding more fields doesn't lead to having to update all callers of New

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 twharmon
Solution 2 Blokje5