Skip to content

Go Option Pattern Explained: Two Elegant Implementations and Best Practices ​

Go Option pattern diagram

In Go development, how to elegantly handle struct configuration is a common question.Option Mode(also known as Functional options pattern) is a widely recognized best practice in the Go community, and it can effectively address pain points such as too many configuration parameters, default value management, and backward compatibility.

This article will explain in detail the two implementations of the Option pattern through practical code examples, compare their applicable scenarios, and help you make the optimal choice in your project.

Why do we need the Option pattern? ​

Before introducing the Option pattern, we usually face the following dilemma:

Drawbacks of traditional solutions ​

Solution 1: Multiple constructors

go
func NewFinder(src string) *Finder
func NewFinderWithExclude(src string, exclude []string) *Finder
func NewFinderWithExtension(src string, extension []string) *Finder
// Combinatorial explosion, difficult to maintain.

Option 2: Configure struct parameters

go
type Config struct {
    Exclude   []string
    Extension []string
    Debug     bool
}
func NewFinder(src string, config Config) *Finder
// Callers need to construct a complete Config, and handling default values is cumbersome.

Advantages of Option mode ​

  • Zero-value safety: Unspecified configurations automatically use default values

  • Composability: Option functions can be combined flexibly.

  • Backward compatibility: Adding new configuration items does not affect existing calling code

  • self-documenting: Function names clearly express configuration intent

1. Implementing the Option pattern with an additional type ​

This implementation approach, by defining independent Option interfaces and internals option Use structs to implement configuration passing, suitable for library or framework development scenarios.

Core implementation code ​

go
package founder

// Option defines the interface for configuration options.
type Option interface {
    apply(option *option)
}

// f is an adapter type that converts a function into an Option
type f func(option *option)

func (f f) apply(option *option) {
    f(option)
}

// option internal configuration struct (private fields)
type option struct {
    exclude   []string
    extension []string // The default value is []string{".go"}
    debug     bool     // The default value is false
}

Configure function definition ​

go
// WithExtension specifies the file extension to search for.
// Supports passing multiple extensions, such as .go, .mod, .yaml
func WithExtension(extension ...string) Option {
    return f(func(option *option) {
        option.extension = extension
    })
}

// WithExclude specifies the names of files or directories to exclude.
// Supports passing multiple exclusions, such as vendor, testdata, node_modules
func WithExclude(exclude ...string) Option {
    return f(func(option *option) {
        option.exclude = exclude
    })
}

// WithDebug sets whether to enable debug mode
// When enabled, detailed logs will be output during the search process.
func WithDebug(debug bool) Option {
    return f(func(option *option) {
        option.debug = debug
    })
}

Main struct and constructor ​

go
type Founder struct {
    src string
    ops *option
}

// NewFounder creates a new file finder
// src: The root directory path to search
// ops: Optional configuration options
func NewFounder(src string, ops ...Option) *Founder {
    // Set default configuration
    var options = &option{extension: []string{".*"}}
    // Apply all options in sequence
    for _, o := range ops {
        o.apply(options)
    }
    return &Founder{src: src, ops: options}
}

Complete usage example ​

go
package main

import "founder"

func main() {
    // Scenario 1: Use the default configuration to find all files
    f1 := NewFounder("./project")

    // Scenario 2: Only search for Go and Mod files, excluding the vendor directory
    f2 := NewFounder("./project",
        WithExtension(".go", ".mod"),
        WithExclude("vendor"),
    )

    // Scenario 3: Enable debug mode for troubleshooting
    f3 := NewFounder("./project",
        WithExtension(".go"),
        WithDebug(true),
    )

    // Use Finder to perform a search...
}

II. Existing Types Implement the Option Pattern ​

This implementation is more concise, directly using function types as Option, suitable Inside the app or Small and medium-sized projects usage scenarios.

Core implementation code ​

go
package finder

// Option is defined as a function type, directly operating on Finder
type Option func(finder *Finder)

type Finder struct {
    exclude   []string
    extension []string // The default value is []string{".go"}
    debug     bool     // The default value is false
    src       string
}

Configuration function definition ​

go
// WithExtension specifies the file extension
func WithExtension(extension ...string) Option {
    return Option(func(finder *Finder) {
        finder.extension = extension
    })
}

// WithExclude specifies the files or directories to exclude.
func WithExclude(exclude ...string) Option {
    return Option(func(finder *Finder) {
        finder.exclude = exclude
    })
}

// WithDebug specifies whether to enable debug mode.
func WithDebug(debug bool) Option {
    return Option(func(finder *Finder) {
        finder.debug = debug
    })
}

Constructor ​

go
// NewFinder creates a new file finder
func NewFinder(src string, ops ...Option) *Finder {
    // Set the default configuration and apply all options.
    var finder = &Finder{
        src:       src,
        extension: []string{".*"},
    }
    for _, o := range ops {
        o(finder)
    }
    return finder
}

Usage example ​

go
package main

import "finder"

func main() {
    // Basic usage
    f1 := NewFinder("./src")

    // Chain configuration, supports any combination
    f2 := NewFinder("./src",
        WithExtension(".go", ".proto"),
        WithExclude("third_party", "build"),
        WithDebug(true),
    )
}

In-depth comparison of two implementation methods ​

Comparison dimensionsAdditional type implementationOriginal type implementation
Code complexity⭐⭐⭐ Relatively high; requires defining an interface + adapter⭐ Minimum, only function type required
Encapsulation⭐⭐⭐⭐⭐ Excellent, configuration fields are completely private⭐⭐⭐ Good, but still pay attention to the export fields.
Scalability⭐⭐⭐⭐⭐ Interface constraints, extended security⭐⭐⭐⭐ Flexible but with weak constraints
Applicable scenariosOpen-source libraries, frameworks, and team infrastructureWithin applications, utility functions, small projects
Learning curveIt is rather steep; you need to understand the adapter pattern.Gentle, easy to understand and use
Test friendlinessHigh, can mock the Option interface.High, functional composition is easy to test.

Production environment best practices ​

1. Configuration validation and defensive programming ​

go
func WithExtension(extension ...string) Option {
    return Option(func(finder *Finder) {
        // Filter out empty strings
        valid := make([]string, 0, len(extension))
        for _, ext := range extension {
            if ext != "" {
                // Ensure the extension starts with a .
                if !strings.HasPrefix(ext, ".") {
                    ext = "." + ext
                }
                valid = append(valid, ext)
            }
        }
        if len(valid) > 0 {
            finder.extension = valid
        }
    })
}

2. Provide convenient preset configuration combinations ​

go
// Preset predefined common configuration combinations
var Preset = struct {
    GoProject  []Option
    AllFiles   []Option
    Production []Option
}{
    GoProject: []Option{
        WithExtension(".go", ".mod", ".sum"),
        WithExclude("vendor", "testdata"),
    },
    AllFiles: []Option{
        WithExtension(".*"),
    },
    Production: []Option{
        WithExtension(".go", ".yaml", ".json"),
        WithExclude("test", "mock", "example"),
        WithDebug(false),
    },
}

// Use preset configuration
finder := NewFinder("./app", Preset.GoProject...)

3. Support merging and overriding of configurations ​

go
// MergeOptions merges multiple option collections; later options override earlier ones.
func MergeOptions(opts ...[]Option) []Option {
    merged := make([]Option, 0)
    for _, optGroup := range opts {
        merged = append(merged, optGroup...)
    }
    return merged
}

// Usage example
baseOpts := []Option{WithExtension(".go")}
extraOpts := []Option{WithDebug(true), WithExclude("vendor")}
allOpts := MergeOptions(baseOpts, extraOpts)
finder := NewFinder("./src", allOpts...)

4. Configuration Delay Verification ​

go
type Finder struct {
    // ... other fields
    validate func() error
}

// WithValidation Add a custom validator
func WithValidation(fn func(finder *Finder) error) Option {
    return Option(func(finder *Finder) {
        finder.validate = fn
    })
}

// Perform verification in NewFinder
func NewFinder(src string, ops ...Option) (*Finder, error) {
    finder := &Finder{src: src, extension: []string{".*"}}
    for _, o := range ops {
        o(finder)
    }
    // Execute verification
    if finder.validate != nil {
        if err := finder.validate(finder); err != nil {
            return nil, err
        }
    }
    return finder, nil
}

FAQ and Pitfall Avoidance Guide ​

Precautions ​

  • Naming convention: Configuration functions are used uniformly.With prefix, such as WithTimeout,WithLogger

  • Default value principle: Select the most commonly used and safest configuration as the default value

  • Immutability: Avoid modifying the configuration object after applying the configuration

  • Concurrency safety: Ensure that the configuration function does not introduce concurrency safety issues

Migration recommendations ​

If you are maintaining an old project, you can migrate incrementally by following these steps:

  • Keep the old constructor and mark it as Deprecated

  • Introduce a new constructor for the Option pattern

  • Internally converts the old configuration into Option calls

  • Remove the old interface in the new version.

Summary ​

The Option pattern is for handling complex configuration in the Go language.Industrial-grade solutions, it has the following core values:

  • Elegant API Design: The caller only needs to focus on the configuration items they care about.

  • Excellent scalability: Adding new options requires no modification to existing code, conforming to the Open-Closed Principle.

  • Comprehensive default value support: Unconfigured parameters automatically use safe default values.

  • Excellent code readability Each configuration function clearly expresses its purpose.

Recommended scenariosSuggested implementation
Open Source Projects / SDK DevelopmentAdditional type implementation
Company internal shared libraryAdditional type implementation
Application layer business codeExisting type implementation
Utility / CLI programImplementation of existing types

Regardless of which implementation approach is chosen, the Option pattern can significantly improve the code's Flexibility and maintainability According to the project's specific requirements and scale, choose the most suitable implementation approach to make your Go code more elegant and robust.

Last updated on11/07/2026, 02:16 PM
@fishfinal | @fishfinals | [email protected]