| 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
optionas 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 ...optionas 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