DEV Community

Cover image for Golang Master Class : Functional Options
Sk
Sk

Posted on

Golang Master Class : Functional Options

Have you ever used a library where the config has like 10+ options?

And here’s the fun part, you really only care about 4 of them. The rest? Optional.

But you still have to configure them anyway. Painful, especially in typed languages where every param has to be accounted for.

Enter functional options.

The idea is simple: any configurable object should be initialized with all its defaults baked in, so the api user only "edits" the parts they care about.

In JavaScript (especially Node.js), you’ve probably seen this kind of pattern with Object.assign:

class Lib {
  config_
  constructor(config = {}) {
    config_ = Object.assign(config_, config) // override only what you care about
  }
}
Enter fullscreen mode Exit fullscreen mode

This works because JS is dynamic. But in Go, we need a pattern to get similar flexibility, and that’s where functional options come in.

Full Code(including prev article):

git clone https://github.com/sklyt/goway.git
Enter fullscreen mode Exit fullscreen mode

Functional Options in Go

Let’s say our Online Store needs an HTTP client.

First, create a new folder in your storage project:

internal/
  client/
    http.go
Enter fullscreen mode Exit fullscreen mode

Now in http.go:

package gohttplib

type Client struct {
    httpClient  *http.Client
    Baseurl     string
    BearerToken string
    Headers     map[string]string
}

// All defaults are initialized here
func NewClient(base string, opts ...Option) *Client {
    c := &Client{
        httpClient:  &http.Client{Timeout: time.Second * 5},
        Baseurl:     base,
        BearerToken: "",
        Headers: map[string]string{
            "X-Custom-Header":  "CustomValue",
            "X-Client-Version": "1.0.0",
            "Cache-Control":    "no-cache",
        },
    }

    for _, o := range opts {
        o(c) // Apply each option
    }

    return c
}
Enter fullscreen mode Exit fullscreen mode

That second parameter here?

opts ...Option
Enter fullscreen mode Exit fullscreen mode

It’s a slice of functions, each one editing the config. This is the magic behind functional options.

Let’s define those functions now.


opts.go

In the same client folder, create opts.go:

package gohttplib

import "time"

type Option func(*Client)

func WithTimeOut(d time.Duration) Option {
    return func(c *Client) {
        c.httpClient.Timeout = d
    }
}

func WithBearerToken(token string) Option {
    return func(c *Client) {
        c.BearerToken = token
    }
}

func WithHeaders(headers map[string]string) Option {
    return func(c *Client) {
        c.Headers = headers
    }
}
Enter fullscreen mode Exit fullscreen mode

Let’s zoom in on WithTimeOut:

func WithTimeOut(d time.Duration) Option {
    return func(c *Client) { c.httpClient.Timeout = d }
}
Enter fullscreen mode Exit fullscreen mode

It returns a function that takes in the client and updates the Timeout.

So when you use it:

gohttplib.NewClient(url, WithTimeOut(100*time.Second))
Enter fullscreen mode Exit fullscreen mode

You’re passing in a function that modifies just the timeout field. Then inside NewClient, we run it:

for _, o := range opts {
    o(c)
}
Enter fullscreen mode Exit fullscreen mode

That’s it!

Because opts is a variadic slice, you can pass in as many With functions as you want.


Putting It Into Online Store

Let’s update the OnlineStore constructor:

func NewOnlineStore(url string, httpOpts ...gohttplib.Option) *OnlineStore {
    return &OnlineStore{
        Logger: logger.NewLogger("ONLINE"),
        Client: gohttplib.NewClient(url, httpOpts...),
    }
}
Enter fullscreen mode Exit fullscreen mode

Now in main.go, we can customize the client like this:

func main() {
    online := storage.NewOnlineStore(
        "https://jsonplaceholder.typicode.com",
        gohttplib.WithTimeOut(2*time.Second),
        gohttplib.WithBearerToken("secret-token-123"),
    )

    // Use online store...
}
Enter fullscreen mode Exit fullscreen mode

Easy. No more bloated config objects or unused boilerplate params.

Top comments (2)

Collapse
 
chariebee profile image
Charles Brown

Love this, but now every time I see more than two arguments in a Go constructor, I get PTSD flashbacks to config hell. Thanks for making my future code reviews just a tiny bit more tolerable!

Collapse
 
sfundomhlungu profile image
Sk

😭😭🤣 Understandable