Golang: Functional Options Pattern

2024-06-26

What is the Functional Options Pattern?

In essence, the functional options pattern in Go is a design approach that enhances the way you create and configure objects. It offers a flexible and readable way to set optional parameters when constructing objects, especially when dealing with numerous configurations.

Key Advantages:

  • Readability: The code becomes more self-explanatory as each option function clearly denotes its purpose.
  • Flexibility: You can easily add, remove, or reorder configuration options without disrupting the existing codebase.
  • Conciseness: It avoids the need for numerous constructors or overloaded functions with varying parameter lists.
  • Defaults: Options can have sensible default values, making the code more user-friendly.

Core Concept:

  1. Option Functions: You define functions that take a pointer to the object being configured and modify its fields accordingly.
  2. Variadic Parameter: The object constructor accepts a variadic parameter (e.g., ...Option) to collect any number of these option functions.
  3. Application: Inside the constructor, you iterate through the provided option functions and apply each one to the object.

Example 1: Configuring a Server

package main

import (
    "fmt"
    "net/http"
)

type Server struct {
    host         string
    port         int
    readTimeout  int
    writeTimeout int
    handler      http.Handler // Use http.Handler interface
}

// Option type (function that modifies Server)
type Option func(*Server)

func NewServer(options ...Option) *Server {
    s := &Server{
        host:         "localhost",
        port:         8080,
        readTimeout:  5, // Default timeouts in seconds
        writeTimeout: 10,
        handler:      http.NotFoundHandler(), // Default 404 handler
    }

    // Apply options
    for _, option := range options {
        option(s)
    }

    return s
}

// Option functions
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 WithTimeouts(readTimeout, writeTimeout int) Option {
    return func(s *Server) {
        s.readTimeout = readTimeout
        s.writeTimeout = writeTimeout
    }
}

func WithHandler(handler http.Handler) Option {
    return func(s *Server) {
        s.handler = handler
    }
}

// Example handler
func myHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello from %s!\n", r.URL.Path)
}

func main() {
    server := NewServer(
        WithHost("127.0.0.1"),
        WithPort(9090),
        WithTimeouts(10, 15),    // Set read and write timeouts
        WithHandler(http.HandlerFunc(myHandler)), // Set custom handler
    )

    // Start the server (not shown for brevity)
    // ... http.ListenAndServe(fmt.Sprintf("%s:%d", server.host, server.port), server.handler) ...
}

Example 2: Configuring a Worker Pool

package main

import (
    "fmt"
    "time"
)

type WorkerPool struct {
    numWorkers   int
    jobChannel   chan func()
    resultChannel chan interface{}
}

type Option func(*WorkerPool)

func NewWorkerPool(numWorkers int, options ...Option) *WorkerPool {
    wp := &WorkerPool{
        numWorkers: numWorkers,
        // Default channels with specific types and sizes
        jobChannel:    make(chan func(), 10),   // Buffered job queue
        resultChannel: make(chan interface{}, 5), // Buffered result queue
    }

    for _, option := range options {
        option(wp)
    }

    // Start the workers
    for i := 0; i < numWorkers; i++ {
        go worker(wp)
    }

    return wp
}

// Option functions to customize channel types and buffer sizes
func WithJobChannel(ch chan func()) Option {
    return func(wp *WorkerPool) {
        wp.jobChannel = ch
    }
}

func WithResultChannel(ch chan interface{}) Option {
    return func(wp *WorkerPool) {
        wp.resultChannel = ch
    }
}

// Worker function
func worker(wp *WorkerPool) {
    for job := range wp.jobChannel {
        result := job()
        wp.resultChannel <- result
    }
}

func main() {
    unbufferedJobChannel := make(chan func())
    bufferedResultChannel := make(chan interface{}, 20)

    pool := NewWorkerPool(5,
        WithJobChannel(unbufferedJobChannel),
        WithResultChannel(bufferedResultChannel),
    )

    // Submit jobs
    pool.jobChannel <- func() {
        time.Sleep(100 * time.Millisecond)
        return "Job 1 result"
    }
    pool.jobChannel <- func() {
        time.Sleep(200 * time.Millisecond)
        return "Job 2 result"
    }

    // Collect results
    for i := 0; i < 2; i++ {
        result := <-pool.resultChannel
        fmt.Println(result)
    }
}