Introduction

Every API, every form submission, every configuration file — they all need validation. In Go, where the language itself provides no built-in validation annotations, the ecosystem has evolved three distinct approaches: go-playground/validator (struct tag-based, declarative), ozzo-validation (programmatic, composable), and govalidator (standalone functions, zero-struct approach).

Choosing the right validation strategy affects not just your code’s correctness but also its readability and testability. This guide compares all three libraries with production-ready code examples.

Library Overview

Featurego-playground/validatorozzo-validationgovalidator
GitHub Stars20,0244,1036,203
ApproachStruct tags (declarative)Programmatic rules (composable)Standalone functions
Type SafetyRuntime (struct tags)Compile-time (typed rules)Runtime (string assertions)
Custom ValidatorsRegister tag functionsCustom Rule interfaceWrap functions
i18n SupportTranslation packageManual via error messagesN/A
Struct-Level ValidationYes (cross-field)Yes (composable)No
PerformanceExcellent (cached reflection)GoodModerate
Active MaintainedYes (committed June 2026)Moderate (last 2024)Yes (committed June 2026)

go-playground/validator: Declarative Tag-Based

go-playground/validator is the de facto standard for Go struct validation with over 20,000 stars. It uses struct tags — a familiar Go pattern also used by encoding/json and database/sql.

Installation:

1
go get github.com/go-playground/validator/v10

Basic Usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type CreateUserRequest struct {
    Name     string `validate:"required,min=2,max=100"`
    Email    string `validate:"required,email"`
    Age      int    `validate:"gte=18,lte=120"`
    Website  string `validate:"omitempty,url"`
    Tags     []string `validate:"min=1,max=5,dive,alphanum"`
    Password string `validate:"required,min=8,containsany=!@#$%^&*"`
}

func main() {
    validate := validator.New()
    req := CreateUserRequest{
        Name:  "J",
        Email: "not-an-email",
        Age:   15,
    }
    err := validate.Struct(req)
    if err != nil {
        for _, e := range err.(validator.ValidationErrors) {
            fmt.Printf("Field %s failed on %s tag\n", e.Field(), e.Tag())
            // Output: Field Name failed on min tag
            //         Field Email failed on email tag
            //         Field Age failed on gte tag
            //         Field Password failed on required tag
        }
    }
}

Custom Validation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Register a custom validator
validate.RegisterValidation("is-cool", func(fl validator.FieldLevel) bool {
    return fl.Field().String() == "very cool"
})

type App struct {
    Status string `validate:"required,is-cool"`
}

// Cross-field validation
type Booking struct {
    CheckIn  time.Time `validate:"required"`
    CheckOut time.Time `validate:"required,gtfield=CheckIn"`
}

Key advantages:

  • Familiar Go idiom: Struct tags are well-understood by every Go developer
  • Rich built-in tags: 50+ validators for strings, numbers, time, network, and more
  • Internationalization: Separate translations package with 15+ languages
  • Dive for slices: dive tag validates each element of slices and maps

ozzo-validation: Programmatic Composable Rules

ozzo-validation takes an object-oriented approach. Instead of struct tags, you build validation rules programmatically by chaining Rule implementations. This is more verbose but gives you full compile-time type checking.

Installation:

1
go get github.com/go-ozzo/ozzo-validation/v4

Basic Usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type Customer struct {
    Name    string
    Email   string
    Address Address
}

type Address struct {
    Street string
    City   string
    Zip    string
}

func (c Customer) Validate() error {
    return validation.ValidateStruct(&c,
        validation.Field(&c.Name, validation.Required, validation.Length(2, 100)),
        validation.Field(&c.Email, validation.Required, is.Email),
        validation.Field(&c.Address),
    )
}

func (a Address) Validate() error {
    return validation.ValidateStruct(&a,
        validation.Field(&a.Street, validation.Required),
        validation.Field(&a.City, validation.Required),
        validation.Field(&a.Zip, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
    )
}

Custom Rules:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Custom validation rule
var IsValidCreditCard = validation.NewStringRuleWithError(
    func(value string) bool {
        // Luhn algorithm check
        return luhnCheck(value)
    },
    validation.NewError("validation_credit_card", "invalid credit card number"),
)

// Composable validation
err := validation.Validate("4111111111111111",
    validation.Required,
    IsValidCreditCard,
)

// Conditional validation
validation.Field(&c.ShippingAddress,
    validation.When(c.HasShipping != "", validation.Required),
)

Key advantages:

  • Type-safe: No string-based tag parsing — all rules are Go functions
  • Composable: Chain rules, nest struct validation, combine with When()
  • IDE-friendly: Autocomplete works on all validation methods
  • Explicit error types: Custom error codes with structured error messages

govalidator: Standalone String-Based Functions

govalidator takes the simplest approach: it provides a rich set of standalone validation functions plus optional struct tag support. It is ideal when you need quick, one-off validations without building a validation framework.

Installation:

1
go get github.com/asaskevich/govalidator

Basic Usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Standalone function calls
if !govalidator.IsEmail("user@example.com") {
    fmt.Println("Invalid email")
}
if !govalidator.IsURL("https://example.com") {
    fmt.Println("Invalid URL")
}
if !govalidator.IsJSON(`{"key": "value"}`) {
    fmt.Println("Invalid JSON")
}
if !govalidator.IsIn("admin", "admin", "user", "moderator") {
    fmt.Println("Unknown role")
}

// Struct tag validation
type User struct {
    Email    string `valid:"email,required"`
    Age      int    `valid:"range(18|120)"`
    IP       string `valid:"ipv4"`
    URL      string `valid:"url"`
    Role     string `valid:"in(admin|user|moderator)"`
}

func main() {
    user := User{
        Email: "bad-email",
        Age:   10,
    }
    ok, err := govalidator.ValidateStruct(user)
    if !ok {
        fmt.Println(err)
    }
}

Key advantages:

  • Zero ceremony: Call govalidator.IsEmail(str) anywhere without struct setup
  • 70+ built-in validators: Credit cards, IP ranges, MAC addresses, Base64, DNS names, etc.
  • Custom validators: Register govalidator.TagMap["my-tag"] = govalidator.Validator(myFunc)
  • Low overhead: No interface implementations required

Key disadvantages:

  • String-based tags: Errors reference tag strings, not Go fields — harder to debug in large structs
  • No composability: Cannot chain or conditionally apply validators programmatically
  • Limited cross-field: No built-in cross-field validation like gtfield

When to Use Each Library

go-playground/validator is the right choice for most Go APIs. Its struct tag approach mirrors the patterns developers already know from json and db tags. It scales well to hundreds of request types because validators are declared alongside the struct fields they validate.

ozzo-validation shines in complex business logic where validation rules depend on runtime state. Its programmatic API makes conditional validation explicit and testable. If you need to validate deeply nested struct hierarchies with branching logic, ozzo’s composability pays off.

govalidator is perfect for simple projects, CLI tools, and cases where you primarily need standalone validation functions. If you find yourself writing if !govalidator.IsURL(url) { ... } scattered through your codebase, it is the lightest-weight solution.

Integration with Web Frameworks

All three libraries integrate with popular Go web frameworks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Gin middleware with go-playground/validator
func validateRequest(c *gin.Context) {
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": "invalid JSON"})
        return
    }
    if err := validate.Struct(req); err != nil {
        c.JSON(422, gin.H{"validation": formatErrors(err)})
        return
    }
    c.Next()
}

// Echo middleware with ozzo-validation
func (h *Handler) CreateCustomer(c echo.Context) error {
    var cust Customer
    c.Bind(&cust)
    if err := cust.Validate(); err != nil {
        return c.JSON(422, map[string]interface{}{"errors": err})
    }
    // proceed...
}

For broader data validation, see our JSON Schema Validation guide. If you need validation at the schema governance level, check our API Schema Validation guide. For Kubernetes-specific validation patterns, our K8s YAML Validation guide covers admission policies.

Validation Performance Considerations

When choosing a validation library, raw throughput is only one factor. Here is what to consider for production Go services:

Memory allocation: go-playground/validator caches reflection metadata after the first validation call on a given struct type, so subsequent validations allocate zero heap — critical for high-throughput APIs handling thousands of requests per second. ozzo-validation allocates a new error container per Validate() call, adding roughly 200 bytes of heap per validation. govalidator’s standalone functions are allocation-light, but its struct tag mode uses reflection without caching.

Startup time: go-playground/validator scans struct tags at first validation call, adding approximately 2ms of init time per unique struct type. This is negligible for long-running servers but matters for CLI tools that validate once and exit. govalidator has near-zero startup cost for standalone function calls.

Error message quality: ozzo-validation produces the most developer-friendly error messages because rules are Go code with explicit error strings. go-playground/validator’s tag-based errors (e.g., “failed on ‘gte’ tag”) require translation for end-user consumption. govalidator returns flat error strings that are straightforward but lack field-path context.

Concurrent safety: go-playground/validator is fully goroutine-safe — its cached reflection metadata is immutable after initialization. ozzo-validation’s Validate() method is safe for concurrent use since validation is a read-only operation on the struct. govalidator’s standalone functions are inherently concurrent-safe.

For most production Go services, the allocation-free nature of go-playground/validator after initial warmup makes it the best choice for sustained throughput. If you are building a CLI tool that validates once at startup, govalidator’s simplicity and zero-setup experience wins.

FAQ

Which Go validation library is fastest?

go-playground/validator is the fastest for struct validation because it caches reflection metadata after the first call. For a 10-field struct, it processes ~2 million validations per second. ozzo-validation adds ~15% overhead from interface dispatch. govalidator is fastest for isolated function calls like IsEmail() since it bypasses struct reflection entirely.

Can I use multiple validation libraries together?

Yes. Many projects use govalidator for quick standalone checks (URL, email) and go-playground/validator for request struct validation in HTTP handlers. They do not conflict because they operate at different abstraction levels.

How do I return structured validation errors to API clients?

All three libraries support custom error formatting. With go-playground/validator, you can extract ValidationErrors and map each to a JSON response with field path, failed tag, and translated message. ozzo-validation returns structured validation.Errors natively. govalidator returns a flat string or Errors slice.

What about validation in Go protobuf/gRPC services?

For gRPC, use protovalidate (Buf’s next-gen protobuf validation) or protoc-gen-validate which generate Go validation code from .proto constraint annotations. These integrate with gRPC interceptors for automatic request validation before your handler executes.

Does go-playground/validator support i18n?

Yes. The github.com/go-playground/validator/v10/translations package provides translation files for Chinese, Japanese, Korean, French, German, Spanish, Portuguese, Russian, Turkish, Indonesian, Vietnamese, and more. You register a translator and use err.Translate(trans) to get localized error messages.

Can I validate deeply nested structs with tags?

Yes. go-playground/validator automatically recurses into nested structs. For slices of structs, use the dive tag: validate:"required,dive". ozzo-validation handles nesting via validation.Field(&s.Nested) where Nested implements the Validatable interface.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com