Agent skill

functions-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.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/functions-methods

SKILL.md

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

go
// 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

go
// 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

go
// 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.

go
// 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.

go
// 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.

go
// 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
go
// 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

go
// 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
go
// 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.

go
// 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

go
// 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

go
// 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

go
// 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

go
// 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

go
// 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.

go
// 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

Pattern Convention
Context parameter Always first
Error return Always last
ID parameters Before data structures
4+ parameters Use parameter struct
Receiver name 1-2 letters, consistent
Pointer receiver For mutation or large structs
Value receiver For small, immutable types
Constructor NewX pattern
Panic constructor MustNewX 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

Didn't find tool you were looking for?

Be as detailed as possible for better results