Common Patterns
Purpose
Document frequently used patterns in RMS Go code. These patterns solve common problems and should be applied consistently.
Enum Pattern
Type-Safe Enums
// DO: Typed string enums
type Status string
const (
StatusPending Status = "PENDING"
StatusInProgress Status = "IN_PROGRESS"
StatusComplete Status = "COMPLETE"
StatusCancelled Status = "CANCELLED"
)
func (s Status) String() string {
return string(s)
}
func (s Status) IsValid() bool {
switch s {
case StatusPending, StatusInProgress, StatusComplete, StatusCancelled:
return true
default:
return false
}
}
func (s Status) IsTerminal() bool {
return s == StatusComplete || s == StatusCancelled
}
func ParseStatus(s string) (Status, error) {
status := Status(s)
if !status.IsValid() {
return "", fmt.Errorf("invalid status: %s", s)
}
return status, nil
}
Integer Enums with iota
// DO: Integer enums for ordered values
type Priority int
const (
PriorityLow Priority = iota + 1
PriorityMedium
PriorityHigh
PriorityCritical
)
func (p Priority) String() string {
switch p {
case PriorityLow:
return "low"
case PriorityMedium:
return "medium"
case PriorityHigh:
return "high"
case PriorityCritical:
return "critical"
default:
return "unknown"
}
}
Retry Pattern
Exponential Backoff
func WithRetry[T any](ctx context.Context, op func() (T, error), opts RetryOptions) (T, error) {
var result T
var lastErr error
for attempt := 0; attempt <= opts.MaxRetries; attempt++ {
result, lastErr = op()
if lastErr == nil {
return result, nil
}
// Don't retry on non-retryable errors
if !opts.IsRetryable(lastErr) {
return result, lastErr
}
if attempt == opts.MaxRetries {
break
}
// Calculate backoff
backoff := opts.BaseDelay * time.Duration(1<<attempt)
if backoff > opts.MaxDelay {
backoff = opts.MaxDelay
}
select {
case <-time.After(backoff):
case <-ctx.Done():
return result, ctx.Err()
}
}
return result, fmt.Errorf("after %d retries: %w", opts.MaxRetries, lastErr)
}
type RetryOptions struct {
MaxRetries int
BaseDelay time.Duration
MaxDelay time.Duration
IsRetryable func(error) bool
}
var DefaultRetryOptions = RetryOptions{
MaxRetries: 3,
BaseDelay: 100 * time.Millisecond,
MaxDelay: 5 * time.Second,
IsRetryable: func(err error) bool {
// Retry transient errors
return !errors.Is(err, ErrNotFound) && !errors.Is(err, ErrInvalidInput)
},
}
Usage
task, err := WithRetry(ctx, func() (*Task, error) {
return client.GetTask(ctx, id)
}, DefaultRetryOptions)
Caching Pattern
Simple In-Memory Cache
type Cache[K comparable, V any] struct {
mu sync.RWMutex
items map[K]cacheItem[V]
ttl time.Duration
}
type cacheItem[V any] struct {
value V
expiresAt time.Time
}
func NewCache[K comparable, V any](ttl time.Duration) *Cache[K, V] {
return &Cache[K, V]{
items: make(map[K]cacheItem[V]),
ttl: ttl,
}
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, ok := c.items[key]
if !ok || time.Now().After(item.expiresAt) {
var zero V
return zero, false
}
return item.value, true
}
func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = cacheItem[V]{
value: value,
expiresAt: time.Now().Add(c.ttl),
}
}
func (c *Cache[K, V]) Delete(key K) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
Get-Or-Set Pattern
func (c *Cache[K, V]) GetOrSet(key K, fetch func() (V, error)) (V, error) {
if v, ok := c.Get(key); ok {
return v, nil
}
v, err := fetch()
if err != nil {
var zero V
return zero, err
}
c.Set(key, v)
return v, nil
}
JSON/YAML Unmarshalling
Strict Unmarshalling
// DO: Use decoder with DisallowUnknownFields
func ParseConfig(r io.Reader) (*Config, error) {
var cfg Config
decoder := json.NewDecoder(r)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&cfg); err != nil {
return nil, fmt.Errorf("decode config: %w", err)
}
return &cfg, nil
}
Custom Unmarshalling
type Duration time.Duration
func (d *Duration) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
duration, err := time.ParseDuration(s)
if err != nil {
return fmt.Errorf("invalid duration: %w", err)
}
*d = Duration(duration)
return nil
}
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).String())
}
Enum Unmarshalling
func (s *Status) UnmarshalJSON(b []byte) error {
var str string
if err := json.Unmarshal(b, &str); err != nil {
return err
}
status := Status(str)
if !status.IsValid() {
return fmt.Errorf("invalid status: %s", str)
}
*s = status
return nil
}
Pagination Pattern
Cursor-Based Pagination
type PageParams struct {
Limit int
Cursor string
}
type PageResult[T any] struct {
Items []T
NextCursor string
HasMore bool
}
func (s *Store) List(ctx context.Context, params PageParams) (*PageResult[*Task], error) {
limit := params.Limit
if limit <= 0 || limit > 100 {
limit = 50
}
// Fetch one extra to determine if there are more
tasks, err := s.query(ctx, params.Cursor, limit+1)
if err != nil {
return nil, err
}
hasMore := len(tasks) > limit
if hasMore {
tasks = tasks[:limit]
}
var nextCursor string
if hasMore && len(tasks) > 0 {
nextCursor = tasks[len(tasks)-1].ID.String()
}
return &PageResult[*Task]{
Items: tasks,
NextCursor: nextCursor,
HasMore: hasMore,
}, nil
}
Builder Pattern
Fluent Builder
type QueryBuilder struct {
filters []string
args []any
orderBy string
limit int
offset int
}
func NewQueryBuilder() *QueryBuilder {
return &QueryBuilder{}
}
func (b *QueryBuilder) Where(condition string, args ...any) *QueryBuilder {
b.filters = append(b.filters, condition)
b.args = append(b.args, args...)
return b
}
func (b *QueryBuilder) OrderBy(column string) *QueryBuilder {
b.orderBy = column
return b
}
func (b *QueryBuilder) Limit(n int) *QueryBuilder {
b.limit = n
return b
}
func (b *QueryBuilder) Build() (string, []any) {
query := "SELECT * FROM tasks"
if len(b.filters) > 0 {
query += " WHERE " + strings.Join(b.filters, " AND ")
}
if b.orderBy != "" {
query += " ORDER BY " + b.orderBy
}
if b.limit > 0 {
query += fmt.Sprintf(" LIMIT %d", b.limit)
}
return query, b.args
}
// Usage
query, args := NewQueryBuilder().
Where("status = ?", "PENDING").
Where("priority >= ?", 3).
OrderBy("created_at DESC").
Limit(50).
Build()
Result Pattern
Result Type for Operations
type Result[T any] struct {
Value T
Err error
}
func (r Result[T]) IsOK() bool {
return r.Err == nil
}
func (r Result[T]) Unwrap() (T, error) {
return r.Value, r.Err
}
// Channel of results
func processAll(ctx context.Context, items []Item) <-chan Result[*Task] {
results := make(chan Result[*Task])
go func() {
defer close(results)
for _, item := range items {
task, err := process(ctx, item)
select {
case results <- Result[*Task]{Value: task, Err: err}:
case <-ctx.Done():
return
}
}
}()
return results
}
Quick Reference
| Pattern | Use Case |
|---|---|
| Enum | Type-safe constants |
| Retry | Transient failures |
| Cache | Expensive computations |
| Pagination | Large datasets |
| Builder | Complex object construction |
| Result | Channel operations with errors |
See Also
- CACHING.md - Advanced caching patterns
- ENUMS.md - Enum implementation details
- functional-options - Option patterns
