
Go Functional Options
- 895 installs
- 137 repo stars
- Updated June 20, 2026
- cxuu/golang-skills
go-functional-options is a Go agent skill that implements the functional options pattern for constructors with optional parameters for developers who need backward-compatible, extensible public APIs without long paramete
About
go-functional-options is a cxuu/golang-skills agent skill sourced from the Uber Go Style Guide and Google Style Guide that teaches the functional options pattern for Go constructors. Its decision framework recommends config structs for internal or test-only APIs, functional options for public APIs with 3+ optional parameters, and validation-friendly option application inside constructors. The pattern uses an unexported options struct, an exported Option interface with unexported apply methods, and With* helper functions applied variadically in New constructors. Developers reach for go-functional-options when designing Connect-style APIs, evolving library constructors, or choosing between functional options, config structs, and builder patterns in Go packages.
- Decision framework based on Google and Uber style guides
- Compares functional options pattern versus config structs across five decision criteria
- Shows when to choose functional options for public APIs with 3+ options or growing configuration
- Demonstrates config struct pattern for internal, test-only, or fixed-option use cases
- Includes concrete good and bad code examples for both patterns
Go Functional Options by the numbers
- 895 all-time installs (skills.sh)
- +39 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #450 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cxuu/golang-skills --skill go-functional-optionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 895 |
|---|---|
| repo stars | ★ 137 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 20, 2026 |
| Repository | cxuu/golang-skills ↗ |
How do you design Go constructors with optional parameters?
Produce clean, extensible Go constructor APIs that support optional parameters without breaking changes.
Who is it for?
Go developers designing public library constructors with 3 or more optional settings that must evolve without breaking callers.
Skip if: Go developers with simple two-parameter constructors or configuration loaded entirely from YAML files where config structs are the wire format.
When should I use this skill?
User designs a Go New function with optional configuration, asks about functional options, or compares options versus config structs.
What you get
A Go constructor using variadic Option functions with defaults, With* helpers, and backward-compatible extension points.
- Option interface and With* helpers
- constructor with applied defaults
By the numbers
- Decision framework targets public APIs with 3+ optional parameters
- Sourced from Uber Go Style Guide and Google Style Guide references
- Compares functional options against config structs and builder patterns
Files
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.
Resource Routing
references/OPTIONS-VS-STRUCTS.md- Read when choosing between config structs and functional options, implementing the full interface-based option pattern, or evaluating hybrid constructor APIs.
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
type Option interface {
apply(*options)
}The unexported apply method ensures only options from this package can be used.
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.
Why Not Closures?
The interface approach is preferred over closure-only options 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
// 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
- [ ]
optionsstruct is unexported - [ ]
Optioninterface has unexportedapplymethod - [ ] 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 when designing the
Optioninterface or choosing between interface and closure approaches - Naming conventions: See go-naming when naming
With*constructors, option types, or the unexported options struct - Function design: See go-functions when organizing constructors within a file or formatting variadic signatures
- Documentation: See go-documentation when documenting
Optiontypes,With*functions, or constructor behavior
External Resources
- Self-referential functions and the design of options - Rob Pike
- Functional options for friendly APIs - Dave Cheney
Functional Options vs Config Structs
Sources: source/google-go-styleguide/best-practices.md; source/uber-go-style/style.md
Authority: project policy
Minimum Go: any supported Go version
Last verified: 2026-06-19
Both functional options and config structs solve the same problem — optional configuration for constructors — but they have different trade-offs. Choose based on API audience, extensibility needs, and complexity budget.
Project policy: use Google guidance to decide whether optional configuration is worth the complexity; when functional options are chosen, prefer Uber's interface-with-unexported-method implementation over closure-only options for debuggability and testability.
Contents
- Decision Framework
- Config Struct Pattern
- Comparison
- When to Prefer Config Structs
- When to Prefer Functional Options
- Caller Ergonomics
- Functional Options Implementation
- Hybrid Approach
Decision Framework
Need optional configuration?
├─ Internal or test-only API?
│ └─ Config struct (simpler, less boilerplate)
├─ Public API with 3+ options?
│ └─ Functional options (extensible, backward-compatible)
├─ Options need validation or interdependencies?
│ └─ Functional options (validate in apply or constructor)
├─ All options usually specified together?
│ └─ Config struct (one literal, no With* ceremony)
└─ Options likely to grow over time?
└─ Functional options (add With* without breaking callers)Config Struct Pattern
A config struct groups optional parameters into a single struct passed to the constructor. Zero values serve as defaults, or provide a DefaultConfig().
Good
type Config struct {
Timeout time.Duration // zero = no timeout
MaxRetry int // zero = no retries
Logger *log.Logger // nil = discard
}
func NewClient(addr string, cfg Config) *Client {
if cfg.Logger == nil {
cfg.Logger = log.New(io.Discard, "", 0)
}
return &Client{addr: addr, cfg: cfg}
}c := NewClient("localhost:8080", Config{
Timeout: 5 * time.Second,
MaxRetry: 3,
})Bad — Relying on unexported config fields in a public API:
type config struct { // unexported: callers can't construct it
timeout time.Duration
}
func NewClient(addr string, cfg config) *Client { ... }When Zero Values Don't Work
If zero is a valid non-default value (e.g., timeout of 0 means "no timeout" but the desired default is 30s), use a pointer field or a sentinel value:
type Config struct {
Timeout *time.Duration // nil = use default (30s), zero = no timeout
}Comparison
| Aspect | Functional Options | Config Struct |
|---|---|---|
| Boilerplate | High (type + apply + With* per option) | Low (one struct) |
| Extensibility | Add With* — no breaking changes | Add field — no breaking changes |
| Backward compat | Excellent for public APIs | Good (new fields get zero values) |
| Defaults | Built into constructor | Zero values or DefaultConfig() |
| Validation | In apply or constructor loop | In constructor after struct received |
| Discoverability | With* functions appear in godoc | All fields visible in one struct |
| Testability | Compare options or test constructor output | Compare struct literals |
| Caller experience | Only specify what differs from defaults | Must construct struct literal |
| Zero-value ambiguity | None — unset options not applied | May need pointer fields |
When to Prefer Config Structs
- Internal APIs — less ceremony, easier to read at call sites
- Few options (1-3) — functional options overhead not justified
- All options typically set together — no benefit to variadic style
- No validation needed — simple field assignment suffices
- Options are data, not behavior — struct fields map naturally
srv := NewServer(Config{
Port: 8080,
TLSCert: "/path/to/cert.pem",
TLSKey: "/path/to/key.pem",
})When to Prefer Functional Options
- Public/library APIs — callers shouldn't track internal config evolution
- 3+ options that are individually optional
- Complex defaults — default computation depends on other options
- Validation per option — reject bad values at apply time
- Options may grow — new
With*functions are purely additive
srv := NewServer(
WithPort(8080),
WithTLS("/path/to/cert.pem", "/path/to/key.pem"),
WithLogger(logger),
)Caller Ergonomics
Functional options are most valuable when they keep call sites focused on the settings that differ from defaults.
Before — positional optional arguments hide meaning and force callers to remember default sentinel values:
conn, err := db.Open(addr, false, zap.NewNop(), 30*time.Second)After — options name each non-default setting and allow the rest to stay inside the constructor:
conn, err := db.Open(
addr,
db.WithCache(false),
db.WithLogger(logger),
)Functional Options Implementation
Use an exported Option interface with an unexported apply method when options should be comparable, mockable, or able to implement extra interfaces:
package db
import "go.uber.org/zap"
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}
}
func Open(addr string, opts ...Option) (*Connection, error) {
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(&options)
}
return &Connection{}, nil
}Avoid closure-only options by default:
type Option func(*options)Closure options are concise, but they are harder to compare in tests, harder to describe in logs, and cannot implement additional interfaces.
Hybrid Approach
For APIs that need both convenience and extensibility, accept a config struct for common settings and functional options for advanced overrides:
func NewServer(cfg Config, opts ...Option) *Server {
s := &Server{cfg: cfg}
for _, o := range opts {
o.apply(&s.cfg)
}
return s
}Use this sparingly — it adds complexity. Prefer one approach per API.
Related skills
How it compares
Pick go-functional-options over config-struct skills when building extensible public Go library constructors expected to gain new knobs over time.
FAQ
When should go-functional-options be used?
go-functional-options recommends functional options for public Go APIs with 3+ optional parameters that must grow without breaking callers, and config structs for internal or serialization-bound configuration.
What structure does go-functional-options define?
go-functional-options defines an unexported options struct, an exported Option interface with an unexported apply method, With* helper functions, and a constructor that applies defaults then iterates variadic options.
Is Go Functional Options safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.