go-argus: A zero-dependency struct validator built on pure Go patterns
go-argus is a struct validation library with an unusually strict constraint: its go.mod only declares the module and Go version. No translator package. No helper validation package. No regex bundle from elsewhere. The validator, rule parser, i18n registry, JSON schema subset, and network helpers all live in the repo.
That makes it useful to read even if you never swap it into an application. It shows what a dependency-free validator has to build for itself, and where the author has chosen compatibility with go-playground/validator over a brand-new API.
The public API is deliberately familiar
The root package is named validator, even though the module path is github.com/kamalyes/go-argus. That is intentional. The README shows a migration path where callers replace the import path and keep the same shape of code:
import validator "github.com/kamalyes/go-argus"
type User struct {
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=150"`
}
func main() {
v := validator.New()
err := v.Struct(User{Name: "A", Email: "bad", Age: -1})
if err != nil {
messages := validator.TranslateValidationErrors(err, "en")
for _, msg := range messages {
fmt.Printf("%s: %s\n", msg.Field, msg.Message)
}
}
}
Two details matter here. The default tag is validate, not a custom argus tag, and callers create a reusable *Validate with validator.New(). That puts go-argus much closer to a drop-in validator replacement than a new DSL.
options.go reinforces that. The options include WithRequiredStructEnabled and WithPrivateFieldValidation, both named to match migration expectations from go-playground/validator. SetTagName and RegisterTagNameFunc live on the validator instance, so projects can keep JSON-field display names or custom tag names without rewriting their structs.
Tags are parsed into small rule plans
The parsing layer sits in rule/. rule.ParseRules turns a tag string into a slice of RulePlan values. Each plan carries the rule name, raw parameter, pre-split parameter parts for rules such as oneof and required_without, and optional OrRules for tags separated with |.
That pre-parsed shape is important because the hot path should not keep splitting strings. In validator.go, struct validation is backed by a structCache, and variable validation has a separate varCache. The code is doing the usual Go performance trade: spend a little effort compiling tags once, then run a compact representation repeatedly.
The rule package is also where cross-field mechanics live. rule/field.go resolves field paths, while field_level.go exposes the custom-rule interface:
type FieldLevel interface {
Top() reflect.Value
Parent() reflect.Value
Field() reflect.Value
FieldName() string
StructFieldName() string
GetTag() string
Param() string
}
That is a practical API. A custom validator can inspect the current field, look at siblings through the parent value, and read the tag parameter without being handed the whole validator internals.
The string fast path is the interesting bit
The repo’s most specific performance idea is VarString. Normal Var validation accepts interface{} and then uses reflection when needed. VarString takes a plain string and tries to validate it through rule.StringRuleMap directly.
The implementation in validator.go is easy to follow. It parses the rule list, skips control rules like omitempty, handles supported string rules without reflection, and only falls back to the reflect path when it hits something that needs more context, such as unsupported cross-field behavior.
That is a sensible compromise. You keep the broad validator API for structs and mixed values, but give simple string validation a path that avoids boxing and reflection. For web services, that can matter because email, URL, UUID, semver, hostname, IP, and similar checks often happen around request boundaries.
The rule implementation is not hidden behind code generation either. rule/string_rules.go and validate/format.go are ordinary Go functions. That makes the performance story inspectable: direct functions for string-compatible checks, reflection only when the rule actually needs it.
Validation checks are split by domain
The validate/ package is organized around the kind of question being asked:
compare.gohandles numeric and string comparisons.empty.gohandles blank, nil, and zero-value checks.enum.goimplements the generic enum validator.format.gocovers email, URL, UUID, base64, semver, cron, ISBN, BIC/SWIFT, and similar formats.network.gocovers IP sets, CIDR handling, wildcards, and network-oriented helpers.json.gohandles JSON and JSON path validation.
That layout is boring in the right way. You can open the file that matches the rule category and find the implementation without chasing a giant registry function. The root validator wires those checks into rule execution; the low-level package stays focused on predicates and comparisons.
The repo also has a schema/ package for a lightweight JSON Schema subset. That is an unusual inclusion for a struct validator, but it fits the project’s stated API-gateway use case: validate request bodies and shape constraints without adding a second dependency.
Built-in translations use plain maps
go-argus ships translations for en, zh, zh-TW, ja, ko, fr, de, es, and ru. The implementation is deliberately small. i18n/i18n.go owns the registry, and each language file registers message templates as Go maps.
For a validation library, that is a reasonable size of abstraction. You do not need pluralization rules or full message catalogs to say “field must be a valid email address.” Keeping the messages in Go also means the package can remain dependency-free and easy to embed in libraries.
The public hooks are still there. RegisterI18n overrides one key, and RegisterI18nMessages registers a batch for a locale. That gives application code a way to adjust wording without forking the library.
Error values are structured
The validation error model is another useful part of the codebase. Instead of returning one concatenated string, go-argus returns ValidationErrors, with field errors carrying details such as namespace, struct namespace, field name, tag, parameter, value, and translated message.
That is the difference between an error you can display and an error you can build a UI or API response from. A handler can turn validation failures into an array of JSON objects without parsing text. The README’s TranslateValidationErrors example leans into that by producing serializable messages for callers.
Where I would use it
For a large production service, go-playground/validator is still the conservative default. It has more users, more battle scars, and a much larger ecosystem around it.
go-argus is compelling in narrower cases: a library that wants validation without adding dependency weight, a small service where built-in translations are enough, or code that validates lots of simple strings and can benefit from the VarString path.
The bigger lesson is architectural. The project shows how to keep a familiar API while replacing external moving parts with small internal packages: cached tag plans, direct string validators, plain-map translations, and structured errors. Those are patterns worth stealing even if your next project has nothing to do with validation.