class-validator (NestJS) → github.com/go-playground/validator/v10
@IsString(), @MinLength(2) → validate:"required,min=2" (теги структур)
@IsEmail() → validate:"email"
class-transformer → encoding/json + ручная валидация
ValidationPipe (NestJS) → middleware с валидацией
mkdir go-validation && cd go-validation
go mod init go-validation
# Устанавливаем validator
go get github.com/go-playground/validator/v10
go get github.com/go-playground/locales/ru
go get github.com/go-playground/universal-translator
go mod tidy
# Структура
mkdir -p internal/validation
mkdir -p internal/middleware
mkdir -p cmd/demo
internal/validation/validator.gopackage validation
import (
"fmt"
"reflect"
"strings"
"github.com/go-playground/locales/ru"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
ruTranslations "github.com/go-playground/validator/v10/translations/ru"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ ГЛОБАЛЬНЫЙ ВАЛИДАТОР ║
// ╚══════════════════════════════════════════════════════════╝
var (
validate *validator.Validate
translator ut.Translator
)
func init() {
// Создаём валидатор
validate = validator.New()
// Настраиваем переводчик (русский язык)
ruLocale := ru.New()
uni := ut.New(ruLocale, ruLocale)
translator, _ = uni.GetTranslator("ru")
// Регистрируем русские переводы
ruTranslations.RegisterDefaultTranslations(validate, translator)
// Регистрируем кастомные валидаторы
registerCustomValidators()
}
// ╔══════════════════════════════════════════════════════════╗
// ║ КАСТОМНЫЕ ВАЛИДАТОРЫ ║
// ╚══════════════════════════════════════════════════════════╝
func registerCustomValidators() {
// Кастомный валидатор: phone (простая проверка)
validate.RegisterValidation("phone", func(fl validator.FieldLevel) bool {
phone := fl.Field().String()
// Простая проверка: начинается с + и содержит минимум 10 цифр
if !strings.HasPrefix(phone, "+") {
return false
}
digits := strings.Map(func(r rune) rune {
if r >= '0' && r <= '9' {
return r
}
return -1
}, phone)
return len(digits) >= 10
})
// Регистрируем перевод для phone
validate.RegisterTranslation("phone", translator,
func(ut ut.Translator) error {
return ut.Add("phone", "должен быть в формате +XXXXXXXXXXX", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("phone", fe.Field())
return t
},
)
// Кастомный валидатор: slug (только латиница, цифры, дефисы)
validate.RegisterValidation("slug", func(fl validator.FieldLevel) bool {
slug := fl.Field().String()
for _, r := range slug {
if !((r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-') {
return false
}
}
return len(slug) > 0
})
// Кастомный валидатор: not_past (дата не в прошлом)
validate.RegisterValidation("not_past", func(fl validator.FieldLevel) bool {
dateStr := fl.Field().String()
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return false
}
return !date.Before(time.Now().Truncate(24 * time.Hour))
})
// Кастомный валидатор: strong_password
validate.RegisterValidation("strong_password", func(fl validator.FieldLevel) bool {
password := fl.Field().String()
var hasUpper, hasLower, hasDigit, hasSpecial bool
for _, ch := range password {
switch {
case unicode.IsUpper(ch): hasUpper = true
case unicode.IsLower(ch): hasLower = true
case unicode.IsDigit(ch): hasDigit = true
case unicode.IsPunct(ch) || unicode.IsSymbol(ch): hasSpecial = true
}
}
return hasUpper && hasLower && hasDigit && hasSpecial
})
}
// Validate — валидирует структуру и возвращает читаемые ошибки
func Validate(s interface{}) error {
err := validate.Struct(s)
if err == nil {
return nil
}
validationErrors, ok := err.(validator.ValidationErrors)
if !ok {
return err
}
// Формируем читаемое сообщение
var messages []string
for _, e := range validationErrors {
messages = append(messages, e.Translate(translator))
}
return fmt.Errorf("validation failed: %s", strings.Join(messages, "; "))
}
// ValidateVar — валидирует одну переменную
func ValidateVar(field interface{}, tag string) error {
return validate.Var(field, tag)
}
// GetValidationErrors — возвращает слайс ошибок для кастомной обработки
func GetValidationErrors(s interface{}) []FieldError {
err := validate.Struct(s)
if err == nil {
return nil
}
validationErrors, ok := err.(validator.ValidationErrors)
if !ok {
return []FieldError{{Field: "unknown", Message: err.Error()}}
}
var errors []FieldError
for _, e := range validationErrors {
errors = append(errors, FieldError{
Field: e.Field(),
Tag: e.Tag(),
Value: fmt.Sprintf("%v", e.Value()),
Message: e.Translate(translator),
})
}
return errors
}
// FieldError — структура ошибки валидации
type FieldError struct {
Field string `json:"field"`
Tag string `json:"tag"`
Value string `json:"value"`
Message string `json:"message"`
}
import (
"time"
"unicode"
)
internal/validation/models.gopackage validation
// ╔══════════════════════════════════════════════════════════╗
// ║ ПРИМЕРЫ СТРУКТУР С ВАЛИДАЦИЕЙ ║
// ╚══════════════════════════════════════════════════════════╝
// CreateUserRequest — запрос на создание пользователя
type CreateUserRequest 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"`
Phone string `json:"phone" validate:"omitempty,phone"` // omitempty — пропустить если пусто
Password string `json:"password" validate:"required,min=8,max=100,strong_password"`
Role string `json:"role" validate:"required,oneof=admin user moderator"`
Slug string `json:"slug" validate:"required,slug"`
Website string `json:"website" validate:"omitempty,url"`
}
// UpdateUserRequest — запрос на обновление (все поля опциональны)
type UpdateUserRequest struct {
Name *string `json:"name" validate:"omitempty,min=2,max=50"`
Email *string `json:"email" validate:"omitempty,email"`
Age *int `json:"age" validate:"omitempty,gte=0,lte=150"`
Phone *string `json:"phone" validate:"omitempty,phone"`
}
// OrderRequest — пример с вложенными структурами
type OrderRequest struct {
CustomerID string `json:"customer_id" validate:"required,uuid"`
Items []OrderItem `json:"items" validate:"required,min=1,max=100,dive"`
// dive — "ныряет" внутрь слайса и валидирует каждый элемент
PromoCode string `json:"promo_code" validate:"omitempty,len=8"`
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
}
// OrderItem — элемент заказа
type OrderItem struct {
ProductID string `json:"product_id" validate:"required,uuid"`
Quantity int `json:"quantity" validate:"required,gt=0,lte=1000"`
Price float64 `json:"price" validate:"required,gt=0"`
}
// SearchRequest — пример условной валидации
type SearchRequest struct {
Query string `json:"query" validate:"required_if=SearchType text"` // Только если SearchType=text
SearchType string `json:"search_type" validate:"required,oneof=text voice image"`
Limit int `json:"limit" validate:"required_with=Offset,omitempty,min=1,max=100"`
// required_with — если Offset задан, то и Limit обязателен
Offset int `json:"offset" validate:"omitempty,min=0"`
}
// DateRangeRequest — валидация дат
type DateRangeRequest struct {
StartDate string `json:"start_date" validate:"required,datetime=2006-01-02"`
EndDate string `json:"end_date" validate:"required,datetime=2006-01-02,gtfield=StartDate"`
// gtfield — EndDate должен быть больше StartDate
}
internal/middleware/validation.gopackage middleware
import (
"encoding/json"
"net/http"
"go-validation/internal/validation"
)
// ValidateBody — middleware для валидации JSON-тела
// Принимает функцию-фабрику для создания нужной структуры
func ValidateBody[T any]() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req T
// Декодируем JSON
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"error": "invalid JSON format",
})
return
}
// Валидируем
if errs := validation.GetValidationErrors(req); len(errs) > 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "validation failed",
"fields": errs,
})
return
}
// В реальности — положить req в контекст
next.ServeHTTP(w, r)
})
}
}
cmd/demo/main.gopackage main
import (
"encoding/json"
"fmt"
"log"
"go-validation/internal/validation"
)
func main() {
fmt.Println("╔══════════════════════════════════════════╗")
fmt.Println("║ ВАЛИДАЦИЯ: GO-PLAYGROUND/VALIDATOR ║")
fmt.Println("╚══════════════════════════════════════════╝")
// ==========================================
// 1. ПРАВИЛЬНЫЙ ЗАПРОС
// ==========================================
fmt.Println("\n── 1. ПРАВИЛЬНЫЙ ЗАПРОС ──")
validReq := validation.CreateUserRequest{
Name: "Alice",
Email: "alice@example.com",
Age: 30,
Phone: "+79001234567",
Password: "StrongPass123!",
Role: "admin",
Slug: "alice-admin",
Website: "https://alice.example.com",
}
if err := validation.Validate(validReq); err != nil {
fmt.Printf(" ❌ Ошибка: %v\n", err)
} else {
fmt.Println(" ✅ Валидация пройдена")
}
// ==========================================
// 2. МНОЖЕСТВО ОШИБОК
// ==========================================
fmt.Println("\n── 2. МНОЖЕСТВО ОШИБОК ──")
invalidReq := validation.CreateUserRequest{
Name: "", // required, min=2
Email: "invalid-email", // email
Age: 200, // lte=150
Password: "weak", // min=8, strong_password
Role: "superadmin", // oneof
Slug: "Invalid Slug!", // slug
}
errors := validation.GetValidationErrors(invalidReq)
fmt.Printf(" Количество ошибок: %d\n", len(errors))
for _, e := range errors {
fmt.Printf(" • %s: %s (тег: %s, значение: %s)\n",
e.Field, e.Message, e.Tag, e.Value)
}
// ==========================================
// 3. ВЛОЖЕННЫЕ СТРУКТУРЫ (dive)
// ==========================================
fmt.Println("\n── 3. ВЛОЖЕННЫЕ СТРУКТУРЫ (dive) ──")
orderReq := validation.OrderRequest{
CustomerID: "550e8400-e29b-41d4-a716-446655440000",
Items: []validation.OrderItem{
{ProductID: "550e8400-e29b-41d4-a716-446655440001", Quantity: 2, Price: 99.99},
{ProductID: "", Quantity: 0, Price: -10}, // Ошибки здесь
},
TotalPrice: 199.98,
}
orderErrors := validation.GetValidationErrors(orderReq)
if len(orderErrors) > 0 {
fmt.Println(" Ошибки в заказе:")
for _, e := range orderErrors {
fmt.Printf(" • %s: %s\n", e.Field, e.Message)
}
}
// ==========================================
// 4. УСЛОВНАЯ ВАЛИДАЦИЯ
// ==========================================
fmt.Println("\n── 4. УСЛОВНАЯ ВАЛИДАЦИЯ ──")
searchReq := validation.SearchRequest{
SearchType: "text",
Query: "", // required_if=SearchType text → ОБЯЗАТЕЛЕН
}
if err := validation.Validate(searchReq); err != nil {
fmt.Printf(" ❌ %v\n", err)
}
// ==========================================
// 5. GT_FIELD (сравнение полей)
// ==========================================
fmt.Println("\n── 5. СРАВНЕНИЕ ПОЛЕЙ (gtfield) ──")
dateReq := validation.DateRangeRequest{
StartDate: "2024-12-31",
EndDate: "2024-01-01", // Меньше StartDate
}
if err := validation.Validate(dateReq); err != nil {
fmt.Printf(" ❌ %v\n", err)
}
// ==========================================
// 6. ВАЛИДАЦИЯ ОДНОГО ПОЛЯ
// ==========================================
fmt.Println("\n── 6. ВАЛИДАЦИЯ ОДНОГО ПОЛЯ ──")
tests := []struct {
value string
tag string
}{
{"user@example.com", "required,email"},
{"not-an-email", "required,email"},
{"", "required,email"},
}
for _, t := range tests {
err := validation.ValidateVar(t.value, t.tag)
if err != nil {
fmt.Printf(" ❌ %q не прошло %q: %v\n", t.value, t.tag, err)
} else {
fmt.Printf(" ✅ %q прошло %q\n", t.value, t.tag)
}
}
// ==========================================
// 7. КРАСИВЫЙ ВЫВОД ОШИБОК (JSON)
// ==========================================
fmt.Println("\n── 7. ОШИБКИ В JSON ──")
jsonErrors, _ := json.MarshalIndent(validation.GetValidationErrors(invalidReq), "", " ")
fmt.Println(string(jsonErrors))
fmt.Println("\n✅ Демонстрация завершена!")
}
go run ./cmd/demo/main.go
| Категория | Теги | Пример |
|---|---|---|
| Обязательность | required, omitempty | validate:"required" |
| Строки | min, max, len, email, url | validate:"min=2,max=50" |
| Числа | gte, lte, gt, lt | validate:"gte=0,lte=150" |
| Ограничения | oneof, eqfield, gtfield | validate:"oneof=admin user" |
| Слайсы/Мапы | min, max, dive, len | validate:"min=1,dive" |
| Условные | required_if, required_with | validate:"required_if=Type text" |
| Форматы | uuid, datetime, json | validate:"uuid" |
dive валидатор проверяет сам слайс, а не элементы.
*string с omitempty — nil не валидируется.💡 Best practices от сеньоров:
💡 Для Node.js разработчика:
class-validator в NestJS использует декораторы. В Go — теги структур.
@IsEmail() → validate:"email". Та же семантика, разный синтаксис.@ValidateNested() → dive. Валидация вложенных объектов.@ValidatorConstraint. В Go — RegisterValidation.