Problem
The current constraint system has two separate code paths for constraint matching:
-
Internal constraints (int, bool, float, alpha, minLen, maxLen, len, betweenLen, min, max, range, datetime, guid, regex) β handled via a TypeConstraint bitmask + a large switch statement in Constraint.CheckConstraint() (path.go:795-932).
-
Custom constraints β implement the CustomConstraint interface (Name() string, Execute(param string, args ...string) bool) but are matched by iterating c.customConstraints at the top of CheckConstraint() before falling through to the switch.
This creates several issues:
- Redundant dispatch logic:
getParamConstraintType() maps names to TypeConstraint constants, then CheckConstraint() maps those constants back to matching logic via switch β two lookups where one would suffice.
- No analysis phase: Built-in constraints like
minLen(3) or range(1,100) parse their integer args via strconv.Atoi() on every request, even though the data is static at route registration time.
- Inconsistent treatment: Any built-in constraint could be expressed as a
CustomConstraint, but the architecture forces them into separate paths.
- Hard to extend: Adding a new built-in constraint requires changes in
constants.go, getParamConstraintType(), CheckConstraint(), and the needOneData/needTwoData bitmasks.
Proposed Solution
Unify all constraints (internal + user-defined) under a single interface with an optional analysis phase:
type Constraint interface {
// Name returns the constraint identifier used in route patterns (e.g. "int", "minLen", "regex").
Name() string
// Execute validates a request parameter value against the constraint.
// precompiled is data produced by Analyse() at registration time (may be nil).
Execute(param string, args []string, precompiled any) bool
}
Constraints that need precomputation (regex compilation, integer parsing, etc.) additionally implement:
type Analyseable interface {
// Analyse preprocesses constraint data at route registration time.
// Returns an opaque value passed to Execute() at match time.
Analyse(args []string) (any, error)
}
Architecture
Registration time:
list := merge(builtinConstraints, userCustomConstraints)
for each constraint in route pattern:
find matching Constraint from list (by Name)
if Analyseable β call Analyse(args), store precompiled data
store {Constraint, args, precompiled} on the route
Request time:
for each constraint on matched segment:
call constraint.Execute(paramValue, args, precompiled)
Key changes
- Remove
TypeConstraint bitmask and the noConstraint/intConstraint/... iota constants.
- Remove the large switch in
CheckConstraint() β each built-in becomes a struct implementing Constraint.
- Remove
customConstraints []CustomConstraint from the Constraint struct β replaced by a single unified lookup.
- Add
Precompiled any field to store analysis results at registration time.
- Each built-in constraint (
intConstraint, minLenConstraint, regexConstraint, etc.) becomes its own type:
type intConstraint struct{}
func (intConstraint) Name() string { return "int" }
func (intConstraint) Execute(param string, _ []string, _ any) bool {
_, err := strconv.Atoi(param)
return err == nil
}
type minLenConstraint struct{}
func (minLenConstraint) Name() string { return "minLen" }
func (minLenConstraint) Analyse(args []string) (any, error) {
return strconv.Atoi(args[0])
}
func (minLenConstraint) Execute(param string, _ []string, pre any) bool {
limit, ok := pre.(int)
if !ok { return false }
return len(param) >= limit
}
Benefits
- Zero allocations at match time:
strconv.Atoi / time.Parse layout parsing / regex compilation all happen once at registration.
- Single dispatch: Replace
TypeConstraint switch + custom constraint loop with one Execute() call.
- Extensibility: Users can register constraints that implement
Analyseable for their own precomputation.
- Simpler code: Remove
TypeConstraint, needOneData, needTwoData, getParamConstraintType(), and the ~130-line switch statement.
Migration / Compatibility
- The public
CustomConstraint interface is preserved (it satisfies the new Constraint interface β Name() and Execute() signatures are compatible).
- The
Analyse() method is optional β constraints that don't need precomputation simply don't implement Analyseable.
- Route pattern syntax (
:param<constraint(data)>) is unchanged.
Scope
This is a significant refactor of path.go. I plan to submit this as a draft PR for review and discussion before finalizing the implementation.
Related: #4387 (routing hot path perf β this issue extracts the constraint optimization that was split out of that PR).
Problem
The current constraint system has two separate code paths for constraint matching:
Internal constraints (
int,bool,float,alpha,minLen,maxLen,len,betweenLen,min,max,range,datetime,guid,regex) β handled via aTypeConstraintbitmask + a largeswitchstatement inConstraint.CheckConstraint()(path.go:795-932).Custom constraints β implement the
CustomConstraintinterface (Name() string,Execute(param string, args ...string) bool) but are matched by iteratingc.customConstraintsat the top ofCheckConstraint()before falling through to the switch.This creates several issues:
getParamConstraintType()maps names toTypeConstraintconstants, thenCheckConstraint()maps those constants back to matching logic via switch β two lookups where one would suffice.minLen(3)orrange(1,100)parse their integer args viastrconv.Atoi()on every request, even though the data is static at route registration time.CustomConstraint, but the architecture forces them into separate paths.constants.go,getParamConstraintType(),CheckConstraint(), and theneedOneData/needTwoDatabitmasks.Proposed Solution
Unify all constraints (internal + user-defined) under a single interface with an optional analysis phase:
Constraints that need precomputation (regex compilation, integer parsing, etc.) additionally implement:
Architecture
Key changes
TypeConstraintbitmask and thenoConstraint/intConstraint/...iota constants.CheckConstraint()β each built-in becomes a struct implementingConstraint.customConstraints []CustomConstraintfrom theConstraintstruct β replaced by a single unified lookup.Precompiled anyfield to store analysis results at registration time.intConstraint,minLenConstraint,regexConstraint, etc.) becomes its own type:Benefits
strconv.Atoi/time.Parselayout parsing / regex compilation all happen once at registration.TypeConstraintswitch + custom constraint loop with oneExecute()call.Analyseablefor their own precomputation.TypeConstraint,needOneData,needTwoData,getParamConstraintType(), and the ~130-line switch statement.Migration / Compatibility
CustomConstraintinterface is preserved (it satisfies the newConstraintinterface βName()andExecute()signatures are compatible).Analyse()method is optional β constraints that don't need precomputation simply don't implementAnalyseable.:param<constraint(data)>) is unchanged.Scope
This is a significant refactor of
path.go. I plan to submit this as a draft PR for review and discussion before finalizing the implementation.Related: #4387 (routing hot path perf β this issue extracts the constraint optimization that was split out of that PR).