Claude Code Plugins

Community-maintained marketplace

Feedback

go-functional-options

@cassamajor/xcnf
1
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name go-functional-options
description 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.

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