Skip to content
Back to skills

Go Functional Options

ASecurity

Use when designing a Go constructor or factory function with optional configuration — especially with 3+ optional parameters or extensible APIs. Also use when building a New* function that takes many settings, even if they don't mention "functional options" by name. Does not cover general function design (see go-functions).

  • 65 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added May 30, 2026
developmentgoapidocumentation

Works with

  • api

Security analysis

A100/100

Scanned May 30, 2026

npx -y skills add ComeOnOliver/skillshub --skill go-functional-options --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Go Functional Options?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Go Functional Options
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/comeonoliver-go-functional-options/badge)](https://www.skillsdirectory.com/skills/comeonoliver-go-functional-options)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: go-functional-options
description: Use when designing a Go constructor or factory function with optional configuration — especially with 3+ optional parameters or extensible APIs. Also use when building a New* function that takes many settings, even if they don't mention "functional options" by name. Does not cover general function design (see go-functions).
license: Apache-2.0
metadata:
  sources: "Uber Style Guide"
---

# Functional Options Pattern

Functional options is a pattern where you declare an opaque `Option` type that records information in an internal struct. The constructor accepts a variadic number of these options and applies them to configure the result.

## When to Use

Use functional options when:

- **3+ optional arguments** on constructors or public APIs
- **Extensible APIs** that may gain new options over time
- **Clean caller experience** is important (no need to pass defaults)

## The Pattern

### Core Components

1. **Unexported `options` struct** - holds all configuration
2. **Exported `Option` interface** - with unexported `apply` method
3. **Option types** - implement the interface
4. **`With*` constructors** - create options

### Option Interface

```go
type Option interface {
    apply(*options)
}
```

The unexported `apply` method ensures only options from this package can be used.

## Complete Implementation

```go
package db

import "go.uber.org/zap"

// options holds all configuration for opening a connection.
type options struct {
    cache  bool
    logger *zap.Logger
}

// Option configures how we open the connection.
type Option interface {
    apply(*options)
}

// cacheOption implements Option for cache setting (simple type alias).
type cacheOption bool

func (c cacheOption) apply(opts *options) {
    opts.cache = bool(c)
}

// WithCache enables or disables caching.
func WithCache(c bool) Option {
    return cacheOption(c)
}

// loggerOption implements Option for logger setting (struct for pointers).
type loggerOption struct {
    Log *zap.Logger
}

func (l loggerOption) apply(opts *options) {
    opts.logger = l.Log
}

// WithLogger sets the logger for the connection.
func WithLogger(log *zap.Logger) Option {
    return loggerOption{Log: log}
}

// Open creates a connection.
func Open(addr string, opts ...Option) (*Connection, error) {
    // Start with defaults
    options := options{
        cache:  defaultCache,
        logger: zap.NewNop(),
    }

    // Apply all provided options
    for _, o := range opts {
        o.apply(&options)
    }

    // Use options.cache and options.logger...
    return &Connection{}, nil
}
```

## Usage Examples

### Without Functional Options (Bad)

```go
// Caller must always provide all parameters, even defaults
db.Open(addr, db.DefaultCache, zap.NewNop())
db.Open(addr, db.DefaultCache, log)
db.Open(addr, false /* cache */, zap.NewNop())
db.Open(addr, false /* cache */, log)
```

### With Functional Options (Good)

```go
// Only provide options when needed
db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(
    addr,
    db.WithCache(false),
    db.WithLogger(log),
)
```

## Comparison: Functional Options vs Config Struct

| Aspect | Functional Options | Config Struct |
|--------|-------------------|---------------|
| **Extensibility** | Add new `With*` functions | Add new fields (may break) |
| **Defaults** | Built into constructor | Zero values or separate defaults |
| **Caller experience** | Only specify what differs | Must construct entire struct |
| **Testability** | Options are comparable | Struct comparison |
| **Complexity** | More boilerplate | Simpler setup |

**Prefer Config Struct when**: Fewer than 3 options, options rarely change, all options usually specified together, or internal APIs only.

> Read [references/OPTIONS-VS-STRUCTS.md](references/OPTIONS-VS-STRUCTS.md) when deciding between functional options and config structs, designing a config struct API with proper defaults, or evaluating the hybrid approach for complex constructors.

## Why Not Closures?

An alternative implementation uses closures:

```go
// Closure approach (not recommended)
type Option func(*options)

func WithCache(c bool) Option {
    return func(o *options) { o.cache = c }
}
```

The interface approach is preferred because:

1. **Testability** - Options can be compared in tests and mocks
2. **Debuggability** - Options can implement `fmt.Stringer`
3. **Flexibility** - Options can implement additional interfaces
4. **Visibility** - Option types are visible in documentation

## Quick Reference

```go
// 1. Unexported options struct with defaults
type options struct {
    field1 Type1
    field2 Type2
}

// 2. Exported Option interface, unexported method
type Option interface {
    apply(*options)
}

// 3. Option type + apply + With* constructor
type field1Option Type1

func (o field1Option) apply(opts *options) { opts.field1 = Type1(o) }
func WithField1(v Type1) Option            { return field1Option(v) }

// 4. Constructor applies options over defaults
func New(required string, opts ...Option) (*Thing, error) {
    o := options{field1: defaultField1, field2: defaultField2}
    for _, opt := range opts {
        opt.apply(&o)
    }
    // ...
}
```

### Checklist

- [ ] `options` struct is unexported
- [ ] `Option` interface has unexported `apply` method
- [ ] Each option has a `With*` constructor
- [ ] Defaults are set before applying options
- [ ] Required parameters are separate from `...Option`

## Related Skills

- **Interface design**: See [go-interfaces](../go-interfaces/SKILL.md) when designing the `Option` interface or choosing between interface and closure approaches
- **Naming conventions**: See [go-naming](../go-naming/SKILL.md) when naming `With*` constructors, option types, or the unexported options struct
- **Function design**: See [go-functions](../go-functions/SKILL.md) when organizing constructors within a file or formatting variadic signatures
- **Documentation**: See [go-documentation](../go-documentation/SKILL.md) when documenting `Option` types, `With*` functions, or constructor behavior

### External Resources

- [Self-referential functions and the design of options](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html) - Rob Pike
- [Functional options for friendly APIs](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) - Dave Cheney

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.