Optional configuration for configuring Go code

Featured image for sharing metadata for article

When you're writing a reusable piece of code in a library, you often want to allow your code to be extensible to a point, in a way that allows safely modify behaviour without exposing too many of the internal workings.

A pattern that seems to be working really well in Go is by providing a varargs option type, when constructing the type, which leads to a method signature like so:

func NewClient(server string, opts ...ClientOption) (*Client, error) {

Since using this - and having the chance to implement it myself - I'm definitely liking it as a pattern.

So how does it work? Let's see how the oapi-codegen's example generated client code does this. The examples are modified for brevity.

By providing ClientOption as a func as the, it allows it to be callable in the construction of NewClient, and modify the underlying Client, and then we can provide some pre-built methods to modify the client.

type Client struct {
	Server string
	Client HttpRequestDoer
	// ...
}

type ClientOption func(*Client) error

func NewClient(server string, opts ...ClientOption) (*Client, error) {
	client := Client{
		Server: server,
	}
	for _, o := range opts {
		if err := o(&client); err != nil {
			return nil, err
		}
	}
  // ...
  return &client, nil
}

func WithHTTPClient(doer HttpRequestDoer) ClientOption {
	return func(c *Client) error {
		c.Client = doer
		return nil
	}
}

func WithRequestEditorFn(fn RequestEditorFn) ClientOption {
	return func(c *Client) error {
		c.RequestEditors = append(c.RequestEditors, fn)
		return nil
	}
}

This works very nicely by having a varargs constructor, so we don't need to add new methods to set up various options.

In the case above, the Client type uses a struct that includes public, exported fields, but that's also not always the case. You may want to hide the implementation details and instead expose an interface, or a struct with a restricted set of public fields.

In that case, we may instead have something like:

type Client interface {
	Do(req *http.Request) (*http.Response, error)
}

type client struct {
	server string
	client HttpRequestDoer
}

type clientOption func(*client) error

func WithHTTPClient(doer HttpRequestDoer) clientOption {
	return func(c *client) error {
		c.Client = doer
		return nil
	}
}

This allows us to continue configuring the client, and doesn't require we make clientOption exported, if we so wish.

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #go.

Also on:

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.