Published on

Go Optional Parameters and the Functional Options Idiom

Authors

In Go you cannot overload methods the way you could in Java or some other languages. You also cannot have methods with default parameters (which is essentially method overloading). But it does have other idioms you can use where you may normally use method overloading.

Today for example I was working on a singleton in a file. I had a LazyInit() method for that instance but I wanted to be able to provide it with a number of ways of configuring the singleton when initializing it. After a bit of searching I came across what is known as the "Functional Options" idiom in Go. Essentially what you do is:

  • Define a type that is a func:
    • This is basically an alias to a function that takes the object you want to configure as an input. You can optionally not do this and instead make the step later just return the alias.
    • This can return anything you may want to work with but ideally it should return nothing and mutate the input going in (generally the struct your are creating a new instance of).
type YourStructOption func(*YourStruct)
  • For the function that you would normally overload in other languages instead make it have a vararg parameter list of the type you defined.
    • I made a utility function that passes an instance of my struct and runs each option callback on it to configure the struct.
    • In Go a method with vararg parameters can take 0 or more inputs of that type as method parameters.
func (YourStruct) newInstance(options ... YourStructOption) YourStruct {
    //do what you need to do to initialize your struct
    //yourStruct = ...

    processOptions(yourStruct, options...)

    return yourStruct
}
  • Define functions that return your newly defined function type or the func signature your type aliases.
    • In the below you could have returned func(*YourStruct) instead but for readability I return YourStructOption.
func DoSomethingToStructInPackage() YourStructOption {
	return func(yourStruct *YourStruct) {
      //do something to yourStruct here
	}
}

There are some views online that using this approach is not as good as just building the struct/option struct directly. Any pattern used incorrectly or/and too liberally becomes an anti pattern which in turn makes code harder instead of easier to read and maintain.

The functional options idiom is a good pattern to use where the struct you need to construct is somewhat complicated to build and has some internal build logic. Using this pattern you can expose configurable options using methods defined as before which are both more readable and easier to maintain as the configuration is contained in the file that uses it - as apposed to scattered across your code when the config struct is built. A more complete example of the snippets is below:

type YourStruct struct {
	*somepackage.StructInPackage
}

type YourStructOption func(*YourStruct)

func (YourStruct) newInstance(options ... YourStructOption) YourStruct {
    //do what you need to do to initialize your struct
    //yourStruct = ...

    processOptions(yourStruct, options...)

    return yourStruct
}

func DoSomethingToStructInPackage() YourStructOption {
	return func(yourStruct *YourStruct) {
		//do something to yourStruct here
	}
}

func processOptions(instanceOfStruct *YourStruct ,options ...YourStructOption) {
	for _, option := range options {
		option(instanceOfStruct)
	}
}