option

package module
v3.0.0-alpha1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 6, 2026 License: MIT Imports: 2 Imported by: 7

README

option

Base object for the "Optional Parameters Pattern".

DESCRIPTION

The beauty of this pattern is that you can achieve a method that can take the following simple calling style

obj.Method(mandatory1, mandatory2)

or the following, if you want to modify its behavior with optional parameters

obj.Method(mandatory1, mandatory2, optional1, optional2, optional3)

Instead of the more clunky zero value for optionals style

obj.Method(mandatory1, mandatory2, nil, "", 0)

or the equally clunky config object style, which requires you to create a struct with `NamesThatLookReallyLongBecauseItNeedsToIncludeMethodNamesConfig

cfg := &ConfigForMethod{
 Optional1: ...,
 Optional2: ...,
 Optional3: ...,
}
obj.Method(mandatory1, mandatory2, &cfg)

SYNOPSIS

Create an "identifier" for the option. We recommend using an unexported empty struct, because

  1. It is uniquely identifiable globally
  2. Takes minimal space
  3. Since it's unexported, you do not have to worry about it leaking elsewhere or having it changed by consumers
// an unexported empty struct
type identFeatureX struct{} 

Then define a method to create an option using this identifier. Here we assume that the option will be a boolean option.

// this is optional, but for readability we usually use a wrapper
// around option.Interface, or a type alias.
type Option = option.Interface

func WithFeatureX(v bool) Option {
  // use the constructor to create a new option
  return option.New(identFeatureX{}, v)
}

Now you can create an option, which essentially a two element tuple consisting of an identifier and its associated value.

To consume this, you will need to create a function with variadic parameters, and iterate over the list looking for a particular identifier:

func MyAwesomeFunc( /* mandatory parameters omitted */, options ...Option) {
  var enableFeatureX bool
  for _, option := range options {
    switch option.Ident() {
    case identFeatureX{}:
      enableFeatureX = option.MustGet[bool](option)
    // other cases omitted
    }
  }
  if enableFeatureX {
    ....
  }
}

Option objects

Option objects take two arguments, its identifier and the value it contains.

The identifier can be anything, but it's usually better to use an unexported empty struct so that only you have the ability to generate said option:

type identOptionalParamOne struct{}
type identOptionalParamTwo struct{}
type identOptionalParamThree struct{}

func WithOptionOne(v ...) Option {
	return option.New(identOptionalParamOne{}, v)
}

Then you can call the method we described above as

obj.Method(m1, m2, WithOptionOne(...), WithOptionTwo(...), WithOptionThree(...))

Options should be parsed in a code that looks somewhat like this

func (obj *Object) Method(m1 Type1, m2 Type2, options ...Option) {
  paramOne := defaultValueParamOne
  for _, option := range options {
    switch option.Ident() {
    case identOptionalParamOne{}:
      paramOne = option.MustGet[ParamOneType](option)
    }
  }
  ...
}

The loop requires a bit of boilerplate, and admittedly, this is the main downside of this module. However, if you think you want use the Option as a Function pattern, please check the FAQ below for rationale.

Extracting values

There are multiple ways to extract the value from an option:

Using Get[T]

Get[T] is a free function that extracts the value with type safety, returning a (T, bool) tuple:

v, ok := option.Get[string](opt)
if !ok {
  // handle type mismatch
}

Using MustGet[T]

MustGet[T] is like Get[T] but panics on type mismatch. Use this inside switch cases on Ident() where the type is guaranteed:

switch opt.Ident() {
case identName{}:
  name := option.MustGet[string](opt)
}

Using Value() on concrete *Option[T]

When you have a concrete *Option[T] (e.g., directly from option.New), you can call the typed Value() method:

opt := option.New(identFoo{}, "hello")
v := opt.Value() // v is string, not any

Simple usage

Most of the times all you need to do is to declare the Option type as an alias in your code:

package myawesomepkg

import "github.com/lestrrat-go/option/v3"

type Option = option.Interface

Then you can start defining options like they are described in the SYNOPSIS section.

Differentiating Options

When you have multiple methods and options, and those options can only be passed to each one the methods, it's hard to see which options should be passed to which method.

func WithX() Option { ... }
func WithY() Option { ... }

// Now, which of WithX/WithY go to which method?
func (*Obj) Method1(options ...Option) {}
func (*Obj) Method2(options ...Option) {}

In this case the easiest way to make it obvious is to put an extra layer around the options so that they have different types

type Method1Option interface {
  Option
  method1Option()
}

type method1Option struct { Option }
func (*method1Option) method1Option() {}

func WithX() Method1Option {
  return &method1Option{option.New(...)}
}

func (*Obj) Method1(options ...Method1Option) {}

This way the compiler knows if an option can be passed to a given method.

FAQ

Why aren't these function-based?

Using a base option type like type Option func(ctx interface{}) is certainly one way to achieve the same goal. In this case, you are giving the option itself the ability to "configure" the main object. For example:

type Foo struct {
  optionaValue bool
}

type Option func(*Foo) error

func WithOptionalValue(v bool) Option {
  return Option(func(f *Foo) error {
    f.optionalValue = v
    return nil
  })
}

func NewFoo(options ...Option) (*Foo, error) {
  var f Foo
  for _, o := range options {
    if err := o(&f); err != nil {
      return nil, err
    }
  }
  return &f
}

This in itself is fine, but we think there are a few problems:

1. It's hard to create a reusable "Option" type

We create many libraries using this optional pattern. We would like to provide a default base object. However, this function based approach is not reusuable because each "Option" type requires that it has a context-specific input type. For example, if the "Option" type in the previous example was func(interface{}) error, then its usability will significantly decrease because of the type conversion.

This is not to say that this library's approach is better as it also requires type conversion to convert the value of the option. However, part of the beauty of the original function based approach was the ease of its use, and we claim that this significantly decreases the merits of the function based approach.

2. The receiver requires exported fields

Part of the appeal for a function-based option pattern is by giving the option itself the ability to do what it wants, you open up the possibility of allowing third-parties to create options that do things that the library authors did not think about.

package thirdparty

func WithMyAwesomeOption( ... ) mypkg.Option  {
  return mypkg.Option(func(f *mypkg) error {
    f.X = ...
    f.Y = ...
    f.Z = ...
    return nil
  })
}

However, for any third party code to access and set field values, these fields (X, Y, Z) must be exported. Basically you will need an "open" struct.

Exported fields are absolutely no problem when you have a struct that represents data alone (i.e., API calls that refer or change state information) happen, but we think that casually expose fields for a library struct is a sure way to maintenance hell in the future. What happens when you want to change the API? What happens when you realize that you want to use the field as state (i.e. use it for more than configuration)? What if they kept referring to that field, and then you have concurrent code accessing it?

Giving third parties complete access to exported fields is like handing out a loaded weapon to the users, and you are at their mercy.

Of course, providing public APIs for everything so you can validate and control concurrency is an option, but then ... it's a lot of work, and you may have to provide APIs only so that users can refer it in the option-configuration phase. That sounds like a lot of extra work.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Get

func Get[T any](opt Interface) (T, bool)

Get extracts the value from an Interface with type safety. Returns the zero value and false if the stored value is not of type T.

func MustGet

func MustGet[T any](opt Interface) T

MustGet extracts the value from an Interface, panicking if the stored value is not of type T. Use this inside switch cases on Ident() where the type is guaranteed.

Types

type Interface

type Interface interface {
	// Ident returns the "identity" of this option, a unique identifier that
	// can be used to differentiate between options
	Ident() any
	// contains filtered or unexported methods
}

Interface defines the minimum interface that an option must fulfill. It uses an unexported method to restrict implementations to this package or types that embed Interface.

type Option

type Option[T any] struct {
	// contains filtered or unexported fields
}

Option is a typed option that stores a value of type T along with an identifier. It implements Interface for heterogeneous storage and provides a typed Value() method for direct access.

func New

func New[T any](ident any, v T) *Option[T]

New creates a new Option with the given identity and value. It returns a concrete *Option[T] so callers can use the typed Value() method. *Option[T] also satisfies Interface for heterogeneous storage.

func (*Option[T]) Ident

func (p *Option[T]) Ident() any

func (*Option[T]) String

func (p *Option[T]) String() string

func (*Option[T]) Value

func (p *Option[T]) Value() T

Value returns the stored value with its original type.

type Set

type Set[T Interface] struct {
	// contains filtered or unexported fields
}

Set is a container to store multiple options. Because options are usually used all over the place to configure various aspects of a system, it is often useful to be able to collect multiple options together and pass them around as a single entity.

Note that Set is meant to be add-only; You usually do not remove options from a Set.

The intention is to create a set using a sync.Pool; we would like to provide a centralized pool of Sets so that you don't need to instantiate a new pool for every type of option you want to store, but that is not quite possible because of the limitations of parameterized types in Go. Instead create a `*option.SetPool` with an appropriate type parameter and allocator.

func NewSet

func NewSet[T Interface]() *Set[T]

func (*Set[T]) Add

func (s *Set[T]) Add(opt T)

func (*Set[T]) Len

func (s *Set[T]) Len() int

func (*Set[T]) List

func (s *Set[T]) List() []T

List returns a slice of all options stored in the Set. Note that the slice is the same slice that is used internally, so you should not modify the contents of the slice directly. This to avoid unnecessary allocations and copying of the slice for performance reasons.

func (*Set[T]) Option

func (s *Set[T]) Option(i int) T

func (*Set[T]) Reset

func (s *Set[T]) Reset()

type SetPool

type SetPool[T Interface] struct {
	// contains filtered or unexported fields
}

SetPool is a pool of Sets that can be used to efficiently manage the lifecycle of Sets. It uses a sync.Pool to store and retrieve Sets, allowing for efficient reuse of memory and reducing the number of allocations required when creating new Sets.

func NewSetPool

func NewSetPool[T Interface](pool *sync.Pool) *SetPool[T]

func (*SetPool[T]) Get

func (p *SetPool[T]) Get() *Set[T]

func (*SetPool[T]) Put

func (p *SetPool[T]) Put(s *Set[T])

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL