Skip to content
go-functional-options logo

Go Functional Options Pattern

go-functional-options

Use the Functional Option Pattern for configurable Go constructors. Applies to types needing multiple optional parameters with validation and defaults. Includes Go 1.25 generics support.

SKILL.md

Full skill instructions

Go Functional Options Pattern

Use the Functional Option Pattern for configurable constructors in Go code. This pattern is recommended for any type that requires configuration with multiple optional parameters.

Go Version

Use Go 1.25 or later for generics support.

Pattern Structure

1. Define Option Function Type

type option func(*targetStruct) error

The option type is always:

  • A function that takes a pointer to your struct
  • Returns an error for validation failures
  • Use lowercase option as the type name

2. Constructor with Variadic Options

func NewThing(opts ...option) (*thing, error) {
    t := &thing{
        // Set sensible defaults first
        input:  os.Stdin,
        output: os.Stdout,
    }
    for _, opt := range opts {
        err := opt(t)
        if err != nil {
            return nil, err
        }
    }
    return t, nil
}

Key points:

  • Accept opts ...option as variadic parameter
  • Initialize struct with sensible defaults before applying options
  • Apply options in order, checking for errors
  • Return both the struct pointer and error

3. Option Factory Functions

func WithInput(input io.Reader) option {
    return func(t *thing) error {
        if input == nil {
            return errors.New("nil input reader")
        }
        t.input = input
        return nil
    }
}

func WithOutput(output io.Writer) option {
    return func(t *thing) error {
        if output == nil {
            return errors.New("nil output writer")
        }
        t.output = output
        return nil
    }
}

func WithConfigFromArgs(args []string) option {
    return func(t *thing) error {
        if len(args) < 1 {
            return nil // Empty args is not an error
        }
        // Process args...
        return nil
    }
}

Naming conventions:

  • Use WithXxx() for option factory functions
  • Use WithXxxFromArgs() when parsing from command-line arguments
  • Return the closure that performs the actual configuration
  • Always validate inputs and return errors for invalid configuration
  • Allow empty/​nil inputs when appropriate (return nil error)

4. Simple Closure Pattern (No Error Returns)

When options are simple value assignments (enums, modes, flags) that cannot fail, use type Option func(*config) without error returns. The constructor handles validation after all options are applied. See EXAMPLES.md for the netkit configuration example.

Usage Examples

Basic Usage

c, err := NewCounter(
    WithInput(inputBuf),
)
if err != nil {
    return err
}

Multiple Options

m, err := NewMatcher(
    WithInput(strings.NewReader(data)),
    WithOutput(outputBuf),
    WithSearchTextFromArgs(os.Args[1:]),
)
if err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
}

With Command-Line Args

func Main() {
    c, err := NewCounter(
        WithInputFromArgs(os.Args[1:]),
    )
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Println(c.Lines())
}

When to Use This Pattern

Use functional options when:

  • Creating types that need configuration with multiple optional parameters
  • You want to provide sensible defaults
  • Configuration may fail and needs validation
  • You want to keep the API flexible for future additions
  • Users should be able to compose configuration options

Don't use when:

  • Only one or two required parameters (use regular function parameters)
  • No configuration needed (simple constructors are fine)
  • The type is too simple to warrant the pattern

Testing Functional Options

Test Individual Options

func TestWithInput_ErrorsOnNilInput(t *testing.T) {
    t.Parallel()
    _, err := NewCounter(
        WithInput(nil),
    )
    if err == nil {
        t.Fatal("want error on nil input, got nil")
    }
}

Test Option Combinations

func TestWithInputFromArgs_IgnoresEmptyArgs(t *testing.T) {
    t.Parallel()
    inputBuf := bytes.NewBufferString("1\n2\n3")
    c, err := NewCounter(
        WithInput(inputBuf),
        WithInputFromArgs([]string{}),
    )
    if err != nil {
        t.Fatal(err)
    }
    // Verify the earlier option (WithInput) is still active
    want := 3
    got := c.Lines()
    if want != got {
        t.Errorf("want %d, got %d", want, got)
    }
}

Test Option Ordering

Options are applied in order, so later options can override earlier ones:

func TestOptionsApplyInOrder(t *testing.T) {
    t.Parallel()
    buf1 := bytes.NewBufferString("first")
    buf2 := bytes.NewBufferString("second")
    c, err := NewCounter(
        WithInput(buf1),
        WithInput(buf2), // This should override buf1
    )
    if err != nil {
        t.Fatal(err)
    }
    // Test should verify buf2 is used
}

Alternative Pattern: Interface-Based Options (Uber Style)

Uber's Go Style Guide recommends an interface-based approach for libraries and public APIs. This alternative provides additional testability and debuggability benefits compared to closures.

Interface-Based Implementation

// package db

type options struct {
    cache  bool
    logger *zap.Logger
}

type Option interface {
    apply(*options)
}

type cacheOption bool

func (c cacheOption) apply(opts *options) {
    opts.cache = bool(c)
}

func WithCache(c bool) Option {
    return cacheOption(c)
}

type loggerOption struct {
    Log *zap.Logger
}

func (l loggerOption) apply(opts *options) {
    opts.logger = l.Log
}

func WithLogger(log *zap.Logger) Option {
    return loggerOption{Log: log}
}

// Open creates a connection.
func Open(addr string, opts ...Option) (*Connection, error) {
    options := options{
        cache:  defaultCache,
        logger: zap.NewNop(),
    }

    for _, o := range opts {
        o.apply(&options)
    }

    // Use options.cache and options.logger
    // ...
}

Usage

db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(addr, db.WithCache(true), db.WithLogger(log))

Advantages: Testable (options comparable), debuggable (can implement fmt.Stringer), flexible (additional interfaces), type-safe

Disadvantages: More verbose, no built-in error handling, more boilerplate

See EXAMPLES.md for a complete database connection example using this pattern.

Choosing Between Closure and Interface Approaches

Use Closure-Based When:

  • Building application code with straightforward configuration needs
  • Error handling during option application is important
  • Working with dynamic validation (e.g., opening files, network checks)
  • Prioritizing simplicity and readability
  • Building CLI tools or internal services

See EXAMPLES.md for complete closure-based examples (line counter, text matcher).

Use Interface-Based (Uber Pattern) When:

  • Building reusable libraries for external consumption
  • Options need to be comparable in tests
  • Debugging option application is critical
  • Options should implement additional interfaces (like fmt.Stringer)
  • Building public APIs expected to expand
  • Error handling can be done before option creation

Error Handling Tradeoff

Closure approach:

func WithInput(input io.Reader) option {
    return func(c *counter) error {
        if input == nil {
            return errors.New("nil input reader")
        }
        c.input = input
        return nil  // Error returned during application
    }
}

Interface approach:

func WithCache(c bool) Option {
    return cacheOption(c)  // No error possible
}

The closure approach allows validation to fail during option application, useful for opening files, validating complex input, or performing I/​O operations. The interface approach requires validation before calling the option factory or in the constructor after all options are applied.

Testing Differences

Closure-Based Testing (test behavior):

func TestWithInput_ErrorsOnNilInput(t *testing.T) {
    t.Parallel()
    _, err := NewCounter(WithInput(nil))
    if err == nil {
        t.Fatal("want error on nil input, got nil")
    }
}

Interface-Based Testing (can compare options):

func TestCacheOptions(t *testing.T) {
    t.Parallel()
    opt1 := db.WithCache(true)
    opt2 := db.WithCache(true)
    // Interface-based options are comparable
    if opt1 != opt2 {
        t.Error("expected equal options")
    }
}

func TestCacheOptionString(t *testing.T) {
    t.Parallel()
    opt := db.WithCache(true)
    // Can implement fmt.Stringer for debugging
    got := opt.String()
    want := "WithCache(true)"
    if got != want {
        t.Errorf("got %q, want %q", got, want)
    }
}

Go 1.25 Features

Go 1.25 introduces generics with type parameters, which can enhance the Functional Options Pattern in some scenarios:

Generic Option Functions

For libraries that need to support multiple similar types, you can create generic option factories:

// Generic option type for any config struct
type Option[T any] func(*T) error

// Generic setter that works with any comparable type
func WithValue[T any, V comparable](setter func(*T, V), value V) Option[T] {
    return func(cfg *T) error {
        setter(cfg, value)
        return nil
    }
}

When to Use Generics with Options

Use generics sparingly with functional options:

  • Do use when creating reusable option utilities across multiple types
  • Don't use for simple, single-type configuration (adds unnecessary complexity)
  • Consider for libraries where the same option pattern applies to multiple config types

For most application code, the non-generic pattern (as shown above) is simpler and more maintainable.

Generic Interface-Based Options (Advanced)

For maximum reusability, combine Uber's interface pattern with Go 1.25 generics to create option utilities that work across multiple configuration types.

Benefits

  • Type-safe reuse: Same option utilities work with different config types
  • Maintains testability: Options are still comparable (unlike generic closures)
  • Option composition: Build reusable option libraries
  • Better type inference: Go can infer types from context

Generic Option Interface

package options

import (
    "fmt"
    "time"
)

// Option is a generic interface for any config type T
type Option[T any] interface {
    apply(*T)
    fmt.Stringer  // Options can be debugged
}

// timeoutOption works with any config type
type timeoutOption[T any] struct {
    duration time.Duration
    setter   func(*T, time.Duration)
}

func (t timeoutOption[T]) apply(cfg *T) {
    t.setter(cfg, t.duration)
}

func (t timeoutOption[T]) String() string {
    return fmt.Sprintf("WithTimeout(%v)", t.duration)
}

// WithTimeout creates a timeout option for any config type
func WithTimeout[T any](
    d time.Duration,
    setter func(*T, time.Duration),
) Option[T] {
    return timeoutOption[T]{duration: d, setter: setter}
}

Usage Across Multiple Types

type DBConfig struct {
    ConnTimeout time.Duration
    MaxConns    int
}

type HTTPConfig struct {
    ReadTimeout time.Duration
    MaxRequests int
}

// Same option utility works with both types
dbOpts := []Option[DBConfig]{
    WithTimeout(5*time.Second, func(c *DBConfig, d time.Duration) {
        c.ConnTimeout = d
    }),
}

httpOpts := []Option[HTTPConfig]{
    WithTimeout(10*time.Second, func(c *HTTPConfig, d time.Duration) {
        c.ReadTimeout = d
    }),
}

When to Use

Use generic interface-based options when:

  • Creating reusable option utility libraries
  • Same option pattern applies to multiple config types
  • Type safety AND testability are both critical
  • Building framework or infrastructure code

Don't use when:

  • Working with a single configuration type (non-generic is simpler)
  • Application-level code (closure-based is more practical)
  • Error handling during option application is needed

See EXAMPLES.md for a complete implementation of generic interface-based options.

Complete Working Examples

For complete, production-ready code examples demonstrating all approaches, see EXAMPLES.md, which includes:

  • Closure-based examples: Line counter and text matcher with error handling
  • Interface-based example: Database connection (Uber style)
  • Generic interface example: Reusable option utilities across multiple types

The examples show:

  • Full implementations with imports and error handling
  • Usage patterns and testing strategies
  • Explanations of when to use each approach
  • Real-world CLI tool and library code

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View