Implements the Chain of Responsibility pattern in Go. Use when the user mentions chain of responsibility, CoR, or when you need to chain handlers that each process and pass to the next—validation pipelines, processing steps, transformation chains, or any sequential pipeline.
Install
npx skillscat add progmichaelkibenko/top-coder-agent-skills/chain-of-responsibility-go Install via the SkillsCat registry.
Chain of Responsibility (Go)
Why: Chain of Responsibility lets you pass a request (or context) along a chain of handlers. Each handler decides whether to process it and pass to the next, or short-circuit. You avoid one big function with all steps and keep each step in its own type (Refactoring.Guru).
Hard constraints: Handlers share a single interface (e.g. Handle(ctx, request)). Each handler holds a reference to the next; the client composes the chain. A handler either processes and passes, or passes without processing.
When to use
- Validation: Multi-rule validation (required → format → range) where you want to add or reorder rules without editing a single validator.
- Any sequential pipeline: Processing steps, transformation chains, or multi-step checks where order matters and each step can process and pass (or stop).
- You want to decouple the sender from concrete handlers and add or reorder steps without changing existing code (Single Responsibility; Open/Closed).
Structure
| Role | Responsibility |
|---|---|
| Handler (interface) | Declares Handle(ctx, request) (and optionally a setter for next). All concrete handlers implement this. |
| Base handler (optional) | Struct that holds Next; default Handle() forwards to Next if non-nil. Reduces boilerplate. |
| Concrete handlers | Implement Handle(). Process the request (e.g. add errors, transform, check); call h.Next.Handle(ctx, request) or return. |
| Client | Builds the chain (e.g. a.SetNext(b); b.SetNext(c)) and invokes the first handler with the initial request. |
A request/context struct is passed through the chain; handlers read it, optionally mutate it, and pass it along (e.g. for validation: Value, FieldName, Errors).
Code contrast (validation example)
Validation is a common use; the same structure applies to any chain. Below: validation.
❌ ANTI-PATTERN: One function with all rules
// One function; every new rule forces edits.
func ValidateOrderInput(data *OrderInput) []ValidationError {
var errs []ValidationError
if data.Email == "" {
errs = append(errs, ValidationError{Field: "email", Message: "email is required"})
} else if !emailRegex.MatchString(data.Email) {
errs = append(errs, ValidationError{Field: "email", Message: "invalid email"})
}
if data.Amount <= 0 || data.Amount > 10000 {
errs = append(errs, ValidationError{Field: "amount", Message: "amount must be 1-10000"})
}
return errs
}Problems: order and logic are hardcoded; adding/removing a rule touches this function; rules are hard to test in isolation; violates Open/Closed.
✅ TOP-CODER PATTERN: Validator interface + base + concrete validators + client-built chain
Validator interface and context:
// chain/validator.go
package chain
type ValidationError struct {
Field string
Message string
}
type Context struct {
Value any
FieldName string
Errors *[]ValidationError
}
type Validator interface {
Validate(ctx *Context)
SetNext(Validator)
}
type BaseValidator struct {
Next Validator
}
func (b *BaseValidator) SetNext(v Validator) {
b.Next = v
}
func (b *BaseValidator) Validate(ctx *Context) {
if b.Next != nil {
b.Next.Validate(ctx)
}
}Concrete validators (embed base, override Validate):
// chain/required.go
type RequiredValidator struct {
chain.BaseValidator
}
func (v *RequiredValidator) Validate(ctx *chain.Context) {
if ctx.Value == nil || strings.TrimSpace(fmt.Sprint(ctx.Value)) == "" {
*ctx.Errors = append(*ctx.Errors, chain.ValidationError{
Field: ctx.FieldName, Message: ctx.FieldName + " is required",
})
}
v.BaseValidator.Validate(ctx)
}
// chain/email_format.go
var emailRegex = regexp.MustCompile(`^[^@]+@[^@]+\.\w+$`)
type EmailFormatValidator struct {
chain.BaseValidator
}
func (v *EmailFormatValidator) Validate(ctx *chain.Context) {
if ctx.Value != nil && ctx.Value != "" && !emailRegex.MatchString(fmt.Sprint(ctx.Value)) {
*ctx.Errors = append(*ctx.Errors, chain.ValidationError{
Field: ctx.FieldName, Message: "invalid email format",
})
}
v.BaseValidator.Validate(ctx)
}
// chain/range.go
type RangeValidator struct {
Min, Max float64
chain.BaseValidator
}
func (v *RangeValidator) Validate(ctx *chain.Context) {
n, ok := toFloat(ctx.Value)
if ctx.Value != nil && (!ok || n < v.Min || n > v.Max) {
*ctx.Errors = append(*ctx.Errors, chain.ValidationError{
Field: ctx.FieldName, Message: fmt.Sprintf("must be between %v and %v", v.Min, v.Max),
})
}
v.BaseValidator.Validate(ctx)
}Client builds one chain per field and runs it:
// service/order.go
emailChain := &chain.RequiredValidator{}
emailChain.SetNext(&chain.EmailFormatValidator{})
amountChain := &chain.RequiredValidator{}
amountChain.SetNext(&chain.RangeValidator{Min: 1, Max: 10000})
func (s *Service) ValidateOrderInput(data *OrderInput) []chain.ValidationError {
var errs []chain.ValidationError
ctx := &chain.Context{Errors: &errs}
ctx.Value, ctx.FieldName = data.Email, "email"
emailChain.Validate(ctx)
ctx.Value, ctx.FieldName = data.Amount, "amount"
amountChain.Validate(ctx)
return errs
}Benefits: add or reorder validators by wiring the chain; each validator is a single type, easy to unit test.
Go notes
- Context struct: Use a shared
Contextwith a pointer toErrorsso all validators append to the same slice. For fail-fast, validators can skip callingv.BaseValidator.Validate(ctx)when they add an error. - Accept interfaces, return structs: The client depends on the
Validatorinterface; concrete types implement it. - Packages: One file per validator when logic is non-trivial (e.g.
chain/required.go); keep the interface and base inchain/validator.go. - No overkill: For one or two fixed steps, a simple function may be enough; use CoR when you have many steps or dynamic composition.
- General chains: Same pattern works for non-validation pipelines (e.g. data transformation, enrichment, multi-step processing)—use a request struct that fits the domain and handlers that process and pass.
Pipeline vs Chain of Responsibility
| Feature | Pipeline | Chain of Responsibility |
|---|---|---|
| Execution | Fixed, mandatory sequence | Conditional; handler decides whether to pass to the next |
| Flow | Linear, no branching | Allows flexible termination and branching |
| Termination | Runs to completion (barring errors) | Can be terminated early by a handler |
| Use cases | Data processing, parsing, ETL | Event handling, approval workflows, validation, message filtering |
Use Pipeline when every stage must run in a fixed order (e.g. data transformation: parse → normalize → enrich → serialize). Use CoR when handlers can short-circuit or decide not to pass (e.g. validation, approval chains).
Reference
- Chain of Responsibility — Refactoring.Guru: intent, problem/solution, structure, applicability, pros/cons, relations with Command/Decorator/Composite.