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

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 dimensions | Additional type implementation | Original 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 scenarios | Open-source libraries, frameworks, and team infrastructure | Within applications, utility functions, small projects |
| Learning curve | It is rather steep; you need to understand the adapter pattern. | Gentle, easy to understand and use |
| Test friendliness | High, 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.
Withprefix, such asWithTimeout,WithLoggerDefault 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
DeprecatedIntroduce 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 scenarios | Suggested implementation |
|---|---|
| Open Source Projects / SDK Development | Additional type implementation |
| Company internal shared library | Additional type implementation |
| Application layer business code | Existing type implementation |
| Utility / CLI program | Implementation 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.