xiaohongshu
xiaohongshu
go-functional-options
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.
Full skill instructions
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.
Use Go 1.25 or later for generics support.
type option func(*targetStruct) error
The option type is always:
option as the type namefunc 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:
opts ...option as variadic parameterfunc 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:
WithXxx() for option factory functionsWithXxxFromArgs() when parsing from command-line argumentsWhen 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.
c, err := NewCounter(
WithInput(inputBuf),
)
if err != nil {
return err
}
m, err := NewMatcher(
WithInput(strings.NewReader(data)),
WithOutput(outputBuf),
WithSearchTextFromArgs(os.Args[1:]),
)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
func Main() {
c, err := NewCounter(
WithInputFromArgs(os.Args[1:]),
)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(c.Lines())
}
Use functional options when:
Don't use when:
func TestWithInput_ErrorsOnNilInput(t *testing.T) {
t.Parallel()
_, err := NewCounter(
WithInput(nil),
)
if err == nil {
t.Fatal("want error on nil input, got nil")
}
}
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)
}
}
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
}
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.
// 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
// ...
}
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.
See EXAMPLES.md for complete closure-based examples (line counter, text matcher).
fmt.Stringer)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.
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 introduces generics with type parameters, which can enhance the Functional Options Pattern in some scenarios:
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
}
}
Use generics sparingly with functional options:
For most application code, the non-generic pattern (as shown above) is simpler and more maintainable.
For maximum reusability, combine Uber's interface pattern with Go 1.25 generics to create option utilities that work across multiple configuration types.
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}
}
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
}),
}
Use generic interface-based options when:
Don't use when:
See EXAMPLES.md for a complete implementation of generic interface-based options.
For complete, production-ready code examples demonstrating all approaches, see EXAMPLES.md, which includes:
The examples show:
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience