Functional Options in GoLang

Functional Options in GoLang

What even are functional options? In Golang, they are a design pattern that accommodates frequently changing parameters or configuration variables for a package. Say you are developing a package and to initialize it, some configurations are needed and you want to make the package as configurable as possible for the client to use. At the same time, you want to make sure you don't introduce breaking changes when a new version of the package is published. What do you do?

Making a new constructor for each combination of configurations is an option but the question is what if you have like a toooooooon of configs? It's not possible or ideal to create that many constructors right? Another way is to create a struct type called "Config" and pass it in the constructor and handle it. Sure this is good when you have a small amount of configs and don't change a lot, but what about breaking changes in the newer version?

This is where functional options come into play. Let's understand the design pattern with an example. Take an example of a package you are creating for initializing a server. Below is the simplest code of server.go file.

package server

import "fmt"

type Server struct {
    host string
    port int
}

func New(host string, port int) *Server {
    return &Server{host: host, port: port}
}

func Start(s *Server) {
    fmt.Printf("Server started at %s:%d\n", s.host, s.port)
}

Here is how your client would initialize the package in main.go :

package main

import "github.com/Atoo35/functionaloptions/server"

func main() {
    s := server.New("localhost", 8080)
    server.Start(s)
}

But what if the client does not pass in the host and the port? Yes, you can easily handle the default case in the New func, but right now we are not allowing for customizability in the code.

Let's use the Functional Options design pattern to fix that. This is what the server.go file would look like:

package server

import "fmt"

type Server struct {
    host string
    port int
}

type Option func(*Server)

func WithHost(host string) Option {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func New(opts ...Option) *Server {
    s := &Server{host: "localhost", port: 8080}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

func Start(s *Server) {
    // start server
    fmt.Printf("Server started at %s:%d\n", s.host, s.port)
}

Let's understand what exactly is happening. First off, we change the New function to be a variadic function. In the first line we set up the server with the default values.

Note: This can be done by using another function that returns the default server to make it better for testing.

Then we loop over the options sent in the function call and apply those options to the server s we initialized before. Since we are passing on the reference of s, we can be sure that the same s has been applied to the options passed in.

Let us understand how is opt in the loop a function we are passing the server type to. We created a type Option just after imports which is a function and accepts the type Server as a parameter.

type Option func(*Server)

This is then used with the other function WithHost which accepts string value in the input and returns the type Option. Remember, the type Option is a function accepting Server which is why the WithHost function needs to return a function accepting Server as a type. This is why we have the return function call inside the function and we are only setting the host property here.

func WithHost(host string) Option {
    return func(s *Server) {
        s.host = host
    }
}

Similarly, we have the WithPort function which sets the port. Since the New function is accepting functions of type Option we can use the passed-in parameters like a function because they indeed are one.

Onto the main.go file now. The file should look like this:

package main

import "github.com/Atoo35/functionaloptions/server"

func main() {
    s := server.New(
        server.WithPort(8081),
    )
    server.Start(s)
}

This is quite similar to how we initialized the server before with the change being the passed-in parameters. Instead of passing in the port, we passed the function WithPort we wrote before with the value 8081. This will set the port value to 8081 and should produce the output as

Since we had configured the server to have default values, host was defaulted to localhost. To add some other host simply pass in the WithHost function in the New function call like this:

s := server.New(
    server.WithPort(8081),
    server.WithHost("mydomain"),
)

The output would look like this:

That's it for today folks. This is how you implement the Functional Options Design Pattern in Go.

I hope this was a good read and would love to get some feedback in the comments!!

Support

If you liked my article, consider supporting me with a coffee ☕️ or some crypto ( ₿, ⟠, etc)

Here is my public address 0x7935468Da117590bA75d8EfD180cC5594aeC1582

Buy Me A Coffee

Let's connect

Github

LinkedIn

Twitter

Feedback

Let me know if I have missed something or provided the wrong info. It helps me keep genuine content and learn.

Did you find this article valuable?

Support Atharva Deshpande by becoming a sponsor. Any amount is appreciated!