Menu
Coddy logo textTech

Functional Options Pattern

Part of the Object Oriented Programming section of Coddy's GO journey — lesson 79 of 107.

When creating structs with many optional configuration fields, constructor functions can become unwieldy. The Functional Options Pattern solves this elegantly by using functions to configure objects.

Consider a server struct with multiple optional settings. Instead of a constructor with many parameters, we define an option type and functions that return options:

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

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

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

The constructor accepts a variadic list of options and applies each one:

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // default
        timeout: 30,      // default
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

Now creating servers becomes readable and flexible:

// Use defaults
s1 := NewServer("localhost")

// Customize specific options
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

This pattern shines because callers only specify what they need, defaults are clear in one place, and adding new options doesn't break existing code. It's widely used in Go libraries like grpc and zap.

challenge icon

Challenge

Easy

Let's build a configurable database connection using the Functional Options Pattern! This pattern shines when you have many optional settings with sensible defaults—exactly what a database connection needs.

You'll organize your code across two files:

  • database.go: Define your database connection type and option functions.

    Create a DBConnection struct with these unexported fields:

    • host (string)
    • port (int)
    • username (string)
    • password (string)
    • maxConnections (int)
    • timeout (int) - in seconds

    Define an Option type as a function that modifies a *DBConnection.

    Create these option functions that return an Option:

    • WithPort(port int)
    • WithCredentials(username, password string)
    • WithMaxConnections(max int)
    • WithTimeout(seconds int)

    Create a constructor NewDBConnection(host string, opts ...Option) that returns a *DBConnection. Set these defaults: port 5432, username "admin", password "secret", maxConnections 10, timeout 30. Apply all provided options after setting defaults.

    Add a ConnectionString() method that returns the connection info in this format:

    [username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)
  • main.go: Build database connections with various configurations.

    Read the host, then read a count of options to apply. For each option, read the option type and its value(s):

    • port followed by the port number
    • credentials followed by username and password (two lines)
    • maxconn followed by the max connections number
    • timeout followed by the timeout in seconds

    Create a DBConnection with the specified options and print its connection string.

The following inputs will be provided:

  • Line 1: Host name
  • Line 2: Number of options
  • Following lines: Option type and value(s)

For example, given:

localhost
0

Your output should be:

admin:secret@localhost:5432 (max:10, timeout:30s)

And given:

db.example.com
2
port
3306
credentials
root
mypassword

Your output should be:

root:mypassword@db.example.com:3306 (max:10, timeout:30s)

And given:

production.server
4
port
5433
credentials
dbuser
securepass123
maxconn
50
timeout
60

Your output should be:

dbuser:securepass123@production.server:5433 (max:50, timeout:60s)

Notice how callers only specify the options they need—unspecified settings use sensible defaults. This is the elegance of the Functional Options Pattern!

Cheat sheet

The Functional Options Pattern provides an elegant way to configure structs with many optional fields by using functions instead of lengthy constructor parameters.

Define an Option type as a function that modifies a pointer to your struct:

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

Create option functions that return Option closures:

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

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

The constructor accepts a variadic list of options, sets defaults, then applies each option:

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // default
        timeout: 30,      // default
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

Usage is clean and flexible—callers only specify what they need:

// Use defaults
s1 := NewServer("localhost")

// Customize specific options
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

Benefits: defaults are centralized, callers specify only what they need, and adding new options doesn't break existing code.

Try it yourself

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// Read host
	host, _ := reader.ReadString('\n')
	host = strings.TrimSpace(host)

	// Read number of options
	numOptionsStr, _ := reader.ReadString('\n')
	numOptions, _ := strconv.Atoi(strings.TrimSpace(numOptionsStr))

	// Collect options
	var opts []Option

	for i := 0; i < numOptions; i++ {
		optionType, _ := reader.ReadString('\n')
		optionType = strings.TrimSpace(optionType)

		switch optionType {
		case "port":
			portStr, _ := reader.ReadString('\n')
			port, _ := strconv.Atoi(strings.TrimSpace(portStr))
			// TODO: Append WithPort option to opts
			_ = port // Remove this line when implementing

		case "credentials":
			username, _ := reader.ReadString('\n')
			username = strings.TrimSpace(username)
			password, _ := reader.ReadString('\n')
			password = strings.TrimSpace(password)
			// TODO: Append WithCredentials option to opts
			_, _ = username, password // Remove this line when implementing

		case "maxconn":
			maxStr, _ := reader.ReadString('\n')
			maxConn, _ := strconv.Atoi(strings.TrimSpace(maxStr))
			// TODO: Append WithMaxConnections option to opts
			_ = maxConn // Remove this line when implementing

		case "timeout":
			timeoutStr, _ := reader.ReadString('\n')
			timeout, _ := strconv.Atoi(strings.TrimSpace(timeoutStr))
			// TODO: Append WithTimeout option to opts
			_ = timeout // Remove this line when implementing
		}
	}

	// TODO: Create DBConnection using NewDBConnection with host and opts
	// TODO: Print the connection string using ConnectionString() method

	_ = opts // Remove this line when implementing
	fmt.Println("TODO: Print connection string here")
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming