Dealing with Optional Parameters in Go

Legend says Go does not support optional parameters or method overloading, as it may be confusing or fragile in practice. However, there are occasions, where optional parameters are useful — often in initialization.

You’ll find some of these practices below, ranging from most verbose options to more parametrized, and extendable. 🚀

Using Multiple Initialization Functions

The simplest solution for dealing with a small number of variables is to use different functions. This technique uses WithX as a convention at the end of the function.

func NewAPIRequest() (*http.Request, error) {
return http.NewRequest(http.MethodGet, ServerURI, nil)
}

This function creates a new HTTP request for the ServerURI which is then returned to the caller. If we want to abstract authorization without the caller knowing how it works, we can simply write the WithAuth function.

func NewRequestWithAuth(key string) (*http.Request, error) {
req, err := NewAPIRequest()
if err != nil {
return nil, err
}
req.Header.Add("authorization", key)

return req, err
}

We are simply adding the provided key into the authorization header, however, this behavior is hidden from the caller.

Pros:
- 📓 Readable and verbose, no hidden logic behind parameters
- ✌️ Great for a small number of variations

Using Structs with Overriden Defaults

You can use the With notation to construct new structs the same way we create requests in the above example. Moreover, you can make parameters with default values private and access them through getters.

type Client struct {
authKey string // private authKey set in constructors
}
func NewClient() *Client { // default constructor
return &Client{}
}
func NewClientWithAuth(key string) *Client { // 'With' Constructor
return &Client{key}
}

Now, because the caller may or may not be bringing her own key, we will want to implement a getter to be used inside our implementation.

func (c *Client) AuthKey() string {
if c.authKey != "" {
return c.authKey
}
return "default-auth-key"
}

Once we have this getter, we are free to use it inside our implementations. We will either use the user-provided key or the default one. Note that you may be using e.g. local machine configuration instead of a hardcoded string here.

func (c *Client) DoSomething() {
// using c.AuthKey() to access the authKey
req, err := NewAPIRequestWithAuth(c.AuthKey())
// ...
}

Pros:
- 🔧 Great for struct initialization
- 9️⃣ Good for a large number of optional parameters that may not be populated by the caller but have a default value

Using Variadic Functions with Option Functions

The last approach can be seen in many libraries using struct initialization. It is widely used as a behavior-extending technique that allows users to pass either options or full functions to modify/extend the behavior of a component.

The example below builds on the Client example and allows us to pass options that modify the authKey as well as options that can modify all requests before they are sent.

type ClientOption func(c *Client)func NewClient(opts ...ClientOption) *Client {
client := &Client{}
for _, opt := range opts {
opt(client)
}

return client
}

This allows us to pass any modifier functions into the NewClient constructor. Instead of writing multiple initializers, we are now free to pass the AuthKey via the ClientOption like so:

func WithAuth(key string) ClientOption {
// this is the ClientOption function type
return func(c *Client) {
c.authKey = key
}
}

Which then results in calling the constructor with the Option:

NewClient(WithAuth("my-authorization-key"))

This method does not only allow us to pass primitives into the initializers. Rather full functions that can modify the internal behavior of the component. You may know this from HTTP frameworks under Middlewares. An example of such as the full implementation can be seen below:

// RequestModifier can modify the http request
type RequestModifier func(r *http.Request)

type Client struct {
// modifiers are applied before any request
modifiers []RequestModifier
}

func NewClient(opts ...RequestModifier) *Client {
c := &Client{}
// register the modifiers
c.modifiers = append(c.modifiers, opts...)

return c
}

// Request constructs a new http request
func (c *Client) Request() (*http.Request, error) {
req, err := NewAPIRequest()
if err != nil {
return nil, err
}

for _, mod := range c.modifiers {
mod(req)
}

return req, nil
}

// WithAuth adds the authorization header to the req
func WithAuth(key string) RequestModifier {
return func(r *http.Request) {
r.Header.Add("authorization", key)
}
}

The example above allows you to pass any number of RequestModifiers into the Client struct which may have additional logic that needs to be performed before the request is returned.

Pros:
- 🎡 Modifying internal behavior in libraries
- ⛺️ Great for covering various unknown use-cases as middlewares do

I will wrap it up here :) If you have any questions, please don’t hesitate to comment or tweet.

I like lasagnas

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store