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.
Scanned 2/12/2026
Install via CLI
openskills install majiayu000/claude-skill-registry---
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
```go
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
```go
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
```go
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](EXAMPLES.md) for the netkit configuration example.
## Usage Examples
### Basic Usage
```go
c, err := NewCounter(
WithInput(inputBuf),
)
if err != nil {
return err
}
```
### Multiple Options
```go
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
```go
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
```go
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
```go
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:
```go
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
```go
// 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
```go
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](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](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:**
```go
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:**
```go
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):
```go
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):
```go
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:
```go
// 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
```go
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
```go
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](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](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
No comments yet. Be the first to comment!