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 RequestModifier
s 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.