description: 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.
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
- Context first -
ctx context.Contextis always the first parameter - Error last -
erroris always the last return value - Limit parameters - Use parameter structs for 4+ arguments
- 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
| 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
- EXAMPLES.md - Extended function examples
- naming-convention - Function naming
- functional-options - Option patterns
- struct-interface - Struct design