opt
A type-safe, generic options pattern implementation for Go that provides a clean and flexible way to configure objects using functional options.
Features
- Type-safe: Uses Go generics for compile-time type safety
- Flexible: Works with any type (structs, interfaces, primitives)
- Composable: Options can be combined and applied in sequence
- Error handling: Built-in error handling for validation and configuration failures
- Zero dependencies: Pure Go implementation with no external dependencies
Installation
go get -u devnw.dev/opt@latest
Quick Start
package main
import (
"fmt"
"log"
"devnw.dev/opt"
)
// Define your configuration struct
type ServerConfig struct {
Host string
Port int
Timeout time.Duration
Debug bool
}
// Create option functions
func WithHost(host string) opt.Option[*ServerConfig] {
return func(config *ServerConfig) error {
if host == "" {
return errors.New("host cannot be empty")
}
config.Host = host
return nil
}
}
func WithPort(port int) opt.Option[*ServerConfig] {
return func(config *ServerConfig) error {
if port <= 0 || port > 65535 {
return errors.New("invalid port range")
}
config.Port = port
return nil
}
}
func WithTimeout(timeout time.Duration) opt.Option[*ServerConfig] {
return func(config *ServerConfig) error {
config.Timeout = timeout
return nil
}
}
func WithDebug(debug bool) opt.Option[*ServerConfig] {
return func(config *ServerConfig) error {
config.Debug = debug
return nil
}
}
// Use the options
func main() {
config := &ServerConfig{
Host: "localhost", // default values
Port: 8080,
}
// Apply options
options := opt.Options[*ServerConfig]{
WithHost("api.example.com"),
WithPort(443),
WithTimeout(30 * time.Second),
WithDebug(true),
}
if err := options.Apply(config); err != nil {
log.Fatal(err)
}
fmt.Printf("Server config: %+v\n", config)
}
API Reference
Types
Option[T any]
type Option[T any] func(T) error
A functional option that takes a value of type T and returns an error if the option application fails.
Methods:
Apply(v T) error - Applies the option to the given value
Options[T any]
type Options[T any] []Option[T]
A slice of options that can be applied sequentially to a value.
Methods:
Apply(v T) error - Applies all options in sequence, stopping on the first error
Usage Patterns
Constructor with Options
func NewServer(opts ...opt.Option[*ServerConfig]) (*Server, error) {
config := &ServerConfig{
Host: "localhost",
Port: 8080,
Timeout: 10 * time.Second,
}
options := opt.Options[*ServerConfig](opts)
if err := options.Apply(config); err != nil {
return nil, err
}
return &Server{config: config}, nil
}
// Usage
server, err := NewServer(
WithHost("api.example.com"),
WithPort(443),
WithDebug(true),
)
Method Chaining Alternative
type ConfigBuilder struct {
config *ServerConfig
}
func NewConfigBuilder() *ConfigBuilder {
return &ConfigBuilder{
config: &ServerConfig{
Host: "localhost",
Port: 8080,
},
}
}
func (cb *ConfigBuilder) Apply(opts ...opt.Option[*ServerConfig]) error {
options := opt.Options[*ServerConfig](opts)
return options.Apply(cb.config)
}
func (cb *ConfigBuilder) Build() *ServerConfig {
return cb.config
}
// Usage
config, err := NewConfigBuilder().
Apply(WithHost("api.example.com")).
Apply(WithPort(443)).
Build()
Conditional Options
func conditionalOptions(isDev bool) []opt.Option[*ServerConfig] {
var opts []opt.Option[*ServerConfig]
if isDev {
opts = append(opts, WithDebug(true))
opts = append(opts, WithHost("localhost"))
} else {
opts = append(opts, WithDebug(false))
opts = append(opts, WithHost("prod.example.com"))
}
return opts
}
// Usage
options := opt.Options[*ServerConfig](conditionalOptions(true))
Error Handling
Options support comprehensive error handling. When an error occurs during option application:
- Single Option: Returns the error immediately
- Multiple Options: Stops at the first error and returns it
- Partial Application: Previously applied options remain in effect
config := &ServerConfig{}
options := opt.Options[*ServerConfig]{
WithHost("valid-host"), // ✓ Applied successfully
WithPort(-1), // ✗ Error: invalid port
WithDebug(true), // ✗ Not reached due to previous error
}
err := options.Apply(config)
// err != nil
// config.Host == "valid-host" (partial application)
// config.Port == 0 (unchanged due to error)
Best Practices
1. Validation in Options
Always validate input in option functions:
func WithPort(port int) opt.Option[*ServerConfig] {
return func(config *ServerConfig) error {
if port <= 0 || port > 65535 {
return fmt.Errorf("invalid port %d: must be between 1 and 65535", port)
}
config.Port = port
return nil
}
}
2. Use Pointer Types
Use pointer types for the target of options to ensure modifications persist:
// ✓ Good - modifications persist
type Option[T any] func(*T) error
// ✗ Avoid - modifications lost due to value copying
type Option[T any] func(T) error
3. Provide Default Values
Set sensible defaults before applying options:
func NewServer(opts ...opt.Option[*ServerConfig]) *Server {
config := &ServerConfig{
Host: "localhost", // Default values
Port: 8080,
Timeout: 30 * time.Second,
Debug: false,
}
// Apply user options
options := opt.Options[*ServerConfig](opts)
options.Apply(config)
return &Server{config: config}
}
4. Compose Options
Create higher-level options by composing simpler ones:
func WithProductionDefaults() opt.Option[*ServerConfig] {
return func(config *ServerConfig) error {
prodOptions := opt.Options[*ServerConfig]{
WithHost("prod.example.com"),
WithPort(443),
WithTimeout(60 * time.Second),
WithDebug(false),
}
return prodOptions.Apply(config)
}
}
Testing
The package includes comprehensive tests demonstrating usage patterns and edge cases. Run tests with:
go test -v ./...
For coverage analysis:
go test -cover ./...
Contributing
This project follows standard Go conventions. Please ensure:
- All code is properly formatted (
go fmt)
- All tests pass (
go test ./...)
- Code coverage remains high
- Documentation is updated for new features
License
See LICENSE file for details.
Generic Makefile
This project includes a generic, reusable Makefile template for Go projects. The template provides:
- Standardized workflow: Consistent targets across all Go projects
- Configurable: Easy customization through variables
- CI/CD ready: Separate targets for continuous integration
- Comprehensive: Covers development, testing, building, and releasing
- Self-documenting: Built-in help and info targets
Quick Setup for New Projects
# Bootstrap the generic Makefile in your Go project
curl -fsSL https://raw.githubusercontent.com/codeprosorg/opt/main/scripts/bootstrap-makefile.sh | bash
# Or manually download the template
curl -fsSL https://raw.githubusercontent.com/codeprosorg/opt/main/Makefile.template > Makefile
Common Commands
make help # Show all available targets
make all # Build, test, and verify project
make test-coverage # Run tests with coverage report
make info # Show project information
See MAKEFILE.md for complete documentation.