Skip to content
Functions and Methods logo

Functions and Methods

Reviews Go function signatures for parameter ordering, return value conventions, and receiver patterns. Use when reviewing function signatures, designing APIs, or seeing complex parameter lists.

SKILL.md

Full skill instructions

Functions and Methods

Purpose

Establish consistent patterns for function and method signatures in RMS Go code. Well-designed function signatures make APIs intuitive and maintainable.

Core Principles

  1. Context first - ctx context.Context is always the first parameter
  2. Error last - error is always the last return value
  3. Limit parameters - Use parameter structs for 4+ arguments
  4. Consistent receivers - Same name across all methods of a type

Parameter Ordering

Standard Order

func Name(ctx context.Context, id rms.ID, options ...Option) (*Result, error)
         ^                     ^          ^                    ^       ^
         |                     |          |                    |       |
         1. Context            2. IDs     3. Variadic opts     4. Data 5. Error

Context Always First

// DO: Context first
func (s *Service) GetTask(ctx context.Context, id rms.ID) (*Task, error)
func (s *Service) CreateTask(ctx context.Context, params CreateTaskParams) (*Task, error)
func (s *Service) List(ctx context.Context, filter TaskFilter) ([]*Task, error)

// DON'T: Context not first
func (s *Service) GetTask(id rms.ID, ctx context.Context) (*Task, error)
func CreateTask(params CreateTaskParams, ctx context.Context) (*Task, error)

IDs Before Data Structures

// DO: IDs before larger structures
func (s *Service) Update(ctx context.Context, id rms.ID, updates TaskUpdates) error
func (s *Service) AssignTask(ctx context.Context, taskID, assigneeID rms.ID) error

// DON'T: Data structures before IDs
func (s *Service) Update(ctx context.Context, updates TaskUpdates, id rms.ID) error

Error Always Last

// DO: Error last in return values
func (s *Service) Get(ctx context.Context, id rms.ID) (*Task, error)
func (s *Service) List(ctx context.Context) ([]*Task, int64, error)

// DON'T: Error not last
func (s *Service) Get(ctx context.Context, id rms.ID) (error, *Task)

Return Values

Named Return Values

Use named returns sparingly - primarily for documentation or defer usage.

// DO: Named returns for complex functions with multiple returns
func (p *Parser) Parse(input string) (result *AST, remaining string, err error) {
    // Named returns document what each value represents
}

// DO: Named returns when defer needs to modify them
func (s *Service) ProcessWithCleanup(ctx context.Context) (err error) {
    lock := s.acquireLock()
    defer func() {
        if unlockErr := lock.Unlock(); unlockErr != nil {
            if err == nil {
                err = unlockErr
            }
        }
    }()
    // ...
}

// DON'T: Named returns for simple functions
func (t *Task) ID() (id rms.ID) {  // Unnecessary
    return t.id
}

// DO: Simple functions without named returns
func (t *Task) ID() rms.ID {
    return t.id
}

Naked Returns

Avoid naked returns - they reduce clarity.

// DON'T: Naked returns
func calculate(a, b int) (result int, err error) {
    result = a + b
    return  // What are we returning?
}

// DO: Explicit returns
func calculate(a, b int) (result int, err error) {
    result = a + b
    return result, nil
}

// Or without named returns
func calculate(a, b int) (int, error) {
    result := a + b
    return result, nil
}

Returning Zero Values

Return explicit zero values when appropriate.

// DO: Return nil explicitly for pointers
func (s *Store) Get(ctx context.Context, id rms.ID) (*Task, error) {
    if !exists {
        return nil, ErrNotFound
    }
    return task, nil
}

// DO: Return empty slice, not nil, when list has no results
func (s *Store) List(ctx context.Context) ([]*Task, error) {
    tasks := make([]*Task, 0)  // Empty slice, not nil
    // ...
    return tasks, nil
}

Parameter Structs

When to Use

Use parameter structs when:

  • Function has 4+ parameters
  • Parameters are logically grouped
  • Function signature changes frequently
  • Optional parameters exist
// DON'T: Too many parameters
func CreateTask(
    ctx context.Context,
    title string,
    description string,
    workflowID rms.ID,
    actorID rms.ID,
    priority Priority,
    dueDate time.Time,
    metadata map[string]any,
) (*Task, error)

// DO: Parameter struct
type CreateTaskParams struct {
    Title       string
    Description string
    WorkflowID  rms.ID
    ActorID     rms.ID
    Priority    Priority
    DueDate     time.Time
    Metadata    map[string]any
}

func CreateTask(ctx context.Context, params CreateTaskParams) (*Task, error)

Parameter Struct Design

// DO: Validate in struct method
type CreateTaskParams struct {
    Title       string         `json:"title"`
    Description string         `json:"description,omitempty"`
    WorkflowID  rms.ID         `json:"workflowId"`
    ActorID     rms.ID         `json:"actorId"`
    Priority    Priority       `json:"priority,omitempty"`
    DueDate     *time.Time     `json:"dueDate,omitempty"`
    Metadata    map[string]any `json:"metadata,omitempty"`
}

func (p CreateTaskParams) Validate() error {
    if p.Title == "" {
        return errors.New("title is required")
    }
    if p.WorkflowID == "" {
        return errors.New("workflow ID is required")
    }
    if p.ActorID == "" {
        return errors.New("actor ID is required")
    }
    return nil
}

// Usage
func (s *Service) CreateTask(ctx context.Context, params CreateTaskParams) (*Task, error) {
    if err := params.Validate(); err != nil {
        return nil, fmt.Errorf("validate params: %w", err)
    }
    // ...
}

Method Receivers

Pointer vs Value Receivers

Use pointer receivers when:

  • Method modifies the receiver
  • Receiver is large (struct with many fields)
  • Consistency - if any method needs pointer, use pointer for all
// DO: Pointer receiver for mutation
func (t *Task) SetStatus(s Status) {
    t.status = s
    t.updatedAt = time.Now()
}

// DO: Pointer receiver for large structs
func (t *Task) Clone() *Task {
    return &Task{
        ID:          t.ID,
        Title:       t.Title,
        Description: t.Description,
        // ...
    }
}

// DO: Value receiver for immutable access on small types
func (s Status) String() string {
    return string(s)
}

func (p Priority) IsHigh() bool {
    return p >= PriorityHigh
}

Receiver Naming

Use short (1-2 letter) names, consistent across all methods.

// DO: Short, consistent receiver names
func (t *Task) ID() rms.ID           { return t.id }
func (t *Task) Status() Status       { return t.status }
func (t *Task) SetStatus(s Status)   { t.status = s }
func (t *Task) Validate() error      { /​* ... */ }
func (t *Task) AddAction(a Action)   { /​* ... */ }

// DON'T: Long or inconsistent names
func (task *Task) ID() rms.ID
func (this *Task) Status() Status
func (t *Task) SetStatus(s Status)
func (tsk *Task) Validate() error

Avoid Self/​This

// DON'T: self/​this are not idiomatic Go
func (self *Task) Validate() error
func (this *Task) Clone() *Task

// DO: Short type-derived name
func (t *Task) Validate() error
func (t *Task) Clone() *Task

Variadic Functions

Use for Optional Parameters

// DO: Variadic for options
func NewTaskFactory(opts ...TaskFactoryOption) *TaskFactory {
    f := &TaskFactory{
        priority: PriorityMedium,
        status:   StatusPending,
    }
    for _, opt := range opts {
        opt(f)
    }
    return f
}

// Usage
factory := NewTaskFactory(
    WithDefaultPriority(PriorityHigh),
    WithValidator(validator),
)

Variadic for Homogeneous Lists

// DO: Variadic for multiple items of same type
func (t *Task) AddTags(tags ...string) {
    t.tags = append(t.tags, tags...)
}

func JoinErrors(errs ...error) error {
    return errors.Join(errs...)
}

// Usage
task.AddTags("urgent", "review-needed", "q4")

Constructor Functions

NewX Pattern

// DO: NewX constructor
func NewTaskService(store TaskStore, opts ...ServiceOption) *TaskService {
    s := &TaskService{
        store:   store,
        logger:  defaultLogger,
        timeout: defaultTimeout,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// DO: NewX with required dependencies
func NewTaskFactory(validator Validator) *TaskFactory {
    return &TaskFactory{
        validator: validator,
        priority:  PriorityMedium,
    }
}

Must Pattern for Initialization

// DO: Must prefix for panic-on-error constructors
func MustNewClient(addr string) *Client {
    client, err := NewClient(addr)
    if err != nil {
        panic(fmt.Sprintf("create client: %v", err))
    }
    return client
}

// Usage in init or main
var client = MustNewClient(os.Getenv("SERVER_ADDR"))

// DON'T: Use Must in regular business logic
func process() {
    client := MustNewClient(addr)  // Dangerous! Will panic on error
}

Function Length

Keep Functions Focused

Functions should do one thing well.

// DON'T: Function doing too much
func (s *Service) ProcessTask(ctx context.Context, id rms.ID) error {
    // Fetch task (10 lines)
    // Validate task (15 lines)
    // Transform data (20 lines)
    // Save to database (10 lines)
    // Publish event (10 lines)
    // Send notification (15 lines)
    return nil
}

// DO: Extract into focused functions
func (s *Service) ProcessTask(ctx context.Context, id rms.ID) error {
    task, err := s.fetchTask(ctx, id)
    if err != nil {
        return fmt.Errorf("fetch task: %w", err)
    }
    
    if err := s.validateTask(task); err != nil {
        return fmt.Errorf("validate: %w", err)
    }
    
    transformed := s.transformTask(task)
    
    if err := s.saveTask(ctx, transformed); err != nil {
        return fmt.Errorf("save: %w", err)
    }
    
    if err := s.publishEvent(ctx, transformed); err != nil {
        return fmt.Errorf("publish: %w", err)
    }
    
    return nil
}

Quick Reference

PatternConvention
Context parameterAlways first
Error returnAlways last
ID parametersBefore data structures
4+ parametersUse parameter struct
Receiver name1-2 letters, consistent
Pointer receiverFor mutation or large structs
Value receiverFor small, immutable types
ConstructorNewX pattern
Panic constructorMustNewX pattern

Function Checklist

  • Context is first parameter?
  • Error is last return value?
  • Parameters under 4, or using struct?
  • Receiver name is short and consistent?
  • Named returns only where needed?
  • No naked returns?

See Also