validator для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Библиотека validator для Go: полное руководство

1. Что это и зачем нужна

Библиотека validator (полное имя пакета — github.com/go-playground/validator/v10) — это мощный и гибкий инструмент для валидации структур и отдельных полей в языке Go. Она позволяет декларативно описывать правила проверки данных прямо в тегах структур, что делает код чистым, читаемым и легко поддерживаемым. Вместо того чтобы писать десятки строк ручных проверок (if-else, switch), вы просто добавляете теги к полям, и библиотека делает всю работу за вас.

Зачем это нужно? В любом веб-приложении, API или сервисе, работающем с пользовательским вводом, критически важно проверять данные до их обработки. Некорректный email, пустое имя, отрицательный возраст, слишком длинный пароль — всё это может привести к ошибкам в логике, уязвимостям (SQL-инъекции, XSS) или падению сервера. validator решает эту проблему элегантно: вы определяете правила один раз, а затем вызываете одну функцию Validate.Struct(), которая проверяет все поля автоматически.

Библиотека поддерживает десятки встроенных валидаторов (required, min, max, email, url, uuid, oneof, len и многие другие), а также позволяет создавать собственные кастомные валидаторы. Она активно используется в продакшене, имеет отличную производительность (благодаря кэшированию рефлексии) и совместима с популярными фреймворками, такими как Gin, Echo и Fiber. Если вы пишете на Go и хотите избежать boilerplate-кода, эта библиотека — must-have.

2. Установка

Установка библиотеки производится стандартным способом через go get. Убедитесь, что у вас установлен Go версии 1.18 или выше (рекомендуется 1.21+).

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

После выполнения команды пакет будет добавлен в ваш go.mod. Импортируйте его в коде:

import "github.com/go-playground/validator/v10"

3. Быстрый старт — минимальный рабочий пример

Давайте создадим простую структуру пользователя и проверим её поля. Это минимальный пример, который демонстрирует основную идею.

package main

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

// User — структура с тегами валидации
type User struct {
    Name  string `validate:"required,min=2,max=50"`
    Email string `validate:"required,email"`
    Age   int    `validate:"gte=0,lte=150"`
}

func main() {
    // Создаём экземпляр валидатора
    validate := validator.New()

    // Тестовые данные
    user := User{
        Name:  "A",               // Слишком короткое имя
        Email: "invalid-email",   // Невалидный email
        Age:   -5,                // Отрицательный возраст
    }

    // Выполняем валидацию
    err := validate.Struct(user)
    if err != nil {
        // Проходим по всем ошибкам
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Printf("Поле: %s, Ошибка: %s, Значение: %v\n",
                err.Field(), err.Tag(), err.Value())
        }
    } else {
        fmt.Println("Валидация пройдена успешно!")
    }
}

Вывод программы:

Поле: Name, Ошибка: min, Значение: A
Поле: Email, Ошибка: email, Значение: invalid-email
Поле: Age, Ошибка: gte, Значение: -5

Как видите, библиотека автоматически находит все нарушения и возвращает структурированную информацию о каждой ошибке.

4. Основные методы и функции

Ниже приведены 8 ключевых методов и функций библиотеки. Для каждого указана сигнатура, описание и рабочий пример.

4.1. validator.New()

Сигнатура: func New() *Validate

Что делает: Создаёт новый экземпляр валидатора. Это основной объект, через который выполняются все проверки. Рекомендуется создавать один экземпляр на всё приложение (singleton).

validate := validator.New()
// Теперь можно вызывать validate.Struct(), validate.Var() и т.д.

4.2. Validate.Struct()

Сигнатура: func (v *Validate) Struct(s interface{}) error

Что делает: Валидирует все поля переданной структуры в соответствии с тегами validate. Возвращает nil, если ошибок нет, или ValidationErrors (срез ошибок).

type Product struct {
    Name  string  `validate:"required"`
    Price float64 `validate:"gte=0"`
}
p := Product{Name: "", Price: -10.5}
err := validate.Struct(p)
if err != nil {
    fmt.Println(err)
}

4.3. Validate.Var()

Сигнатура: func (v *Validate) Var(field interface{}, tag string) error

Что делает: Валидирует одно значение (не структуру) по указанному тегу. Полезно для проверки отдельных переменных, например, строк или чисел.

email := "test@example"
err := validate.Var(email, "required,email")
if err != nil {
    fmt.Println("Email невалиден:", err)
} else {
    fmt.Println("Email корректен")
}

4.4. Validate.VarWithValue()

Сигнатура: func (v *Validate) VarWithValue(field interface{}, other interface{}, tag string) error

Что делает: Валидирует поле относительно другого значения. Используется для сравнения, например, проверка совпадения паролей или диапазона дат.

password := "secret123"
confirm := "secret456"
err := validate.VarWithValue(confirm, password, "eqfield")
if err != nil {
    fmt.Println("Пароли не совпадают:", err)
}

4.5. Validate.RegisterValidation()

Сигнатура: func (v *Validate) RegisterValidation(tag string, fn Func, callValidationEvenIfNull ...bool) error

Что делает: Регистрирует кастомный валидатор с уникальным именем тега. Функция fn должна иметь сигнатуру func(fl validator.FieldLevel) bool.

// Кастомный валидатор: строка должна содержать слово "go"
validate.RegisterValidation("contains_go", func(fl validator.FieldLevel) bool {
    value := fl.Field().String()
    return strings.Contains(value, "go")
})

type Post struct {
    Title string `validate:"required,contains_go"`
}
p := Post{Title: "Hello World"}
err := validate.Struct(p)
if err != nil {
    fmt.Println(err) // Ошибка: Title не содержит "go"
}

4.6. Validate.RegisterAlias()

Сигнатура: func (v *Validate) RegisterAlias(alias, tags string)

Что делает: Создаёт псевдоним (алиас) для комбинации тегов. Упрощает повторное использование сложных правил.

// Алиас "notempty" = "required" + "min=1"
validate.RegisterAlias("notempty", "required,min=1")

type Config struct {
    APIKey string `validate:"notempty"`
}
c := Config{APIKey: ""}
err := validate.Struct(c)
if err != nil {
    fmt.Println(err) // Ошибка: APIKey не соответствует required или min
}

4.7. Validate.RegisterCustomTypeFunc()

Сигнатура: func (v *Validate) RegisterCustomTypeFunc(fn CustomTypeFunc, types ...interface{})

Что делает: Регистрирует функцию для извлечения значения из кастомного типа (например, обёртки над временем или ID). Позволяет валидировать поля, которые не являются примитивами.

type MyTime struct {
    time.Time
}

// Функция извлечения значения
validate.RegisterCustomTypeFunc(func(field reflect.Value) interface{} {
    if t, ok := field.Interface().(MyTime); ok {
        return t.Unix()
    }
    return nil
}, MyTime{})

type Event struct {
    Start MyTime `validate:"required"`
}

4.8. Validate.RegisterStructValidation()

Сигнатура: func (v *Validate) RegisterStructValidation(fn StructLevelFunc, types ...interface{})

Что делает: Регистрирует валидатор на уровне всей структуры. Позволяет проверять взаимосвязи между полями (например, дата начала не позже даты окончания).

type DateRange struct {
    Start string `validate:"required"`
    End   string `validate:"required"`
}

validate.RegisterStructValidation(func(sl validator.StructLevel) {
    dr := sl.Current().Interface().(DateRange)
    if dr.Start > dr.End {
        sl.ReportError(dr.Start, "Start", "Start", "before_end", "")
    }
}, DateRange{})

dr := DateRange{Start: "2025-03-01", End: "2025-02-01"}
err := validate.Struct(dr)
if err != nil {
    fmt.Println(err) // Ошибка: Start не соответствует before_end
}

5. Реальный практический пример

Рассмотрим полноценный пример: REST API для регистрации пользователя с валидацией всех полей, включая кастомные проверки.

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "regexp"
    "strings"
    "time"

    "github.com/go-playground/validator/v10"
)

// RegisterRequest — структура запроса на регистрацию
type RegisterRequest struct {
    Username     string   `json:"username" validate:"required,min=3,max=30,alphanum"`
    Email        string   `json:"email" validate:"required,email"`
    Password     string   `json:"password" validate:"required,min=8,max=64"`
    ConfirmPass  string   `json:"confirm_password" validate:"required,eqfield=Password"`
    Age          int      `json:"age" validate:"gte=18,lte=120"`
    Phone        string   `json:"phone" validate:"required,e164"`
    Country      string   `json:"country" validate:"required,iso3166_1_alpha2"`
    AgreedToTerms bool    `json:"agreed_to_terms" validate:"eq=true"`
    ReferralCode string   `json:"referral_code" validate:"omitempty,len=8"`
}

// Response — структура ответа API
type Response struct {
    Success bool        `json:"success"`
    Message string      `json:"message,omitempty"`
    Errors  []FieldError `json:"errors,omitempty"`
}

type FieldError struct {
    Field   string `json:"field"`
    Tag     string `json:"tag"`
    Value   string `json:"value"`
}

var validate *validator.Validate

func init() {
    validate = validator.New()

    // Кастомный валидатор для проверки сложности пароля
    validate.RegisterValidation("password_strength", func(fl validator.FieldLevel) bool {
        password := fl.Field().String()
        hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
        hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
        hasDigit := regexp.MustCompile(`[0-9]`).MatchString(password)
        hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]`).MatchString(password)
        return hasUpper && hasLower && hasDigit && hasSpecial
    })

    // Регистрируем кастомный тег для пароля
    validate.RegisterAlias("strongpassword", "required,min=8,max=64,password_strength")
}

func registerHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    var req RegisterRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(Response{
            Success: false,
            Message: "Invalid JSON body",
        })
        return
    }

    // Валидация структуры
    err := validate.Struct(req)
    if err != nil {
        var fieldErrors []FieldError
        for _, verr := range err.(validator.ValidationErrors) {
            fieldErrors = append(fieldErrors, FieldError{
                Field: verr.Field(),
                Tag:   verr.Tag(),
                Value: fmt.Sprintf("%v", verr.Value()),
            })
        }
        w.WriteHeader(http.StatusUnprocessableEntity)
        json.NewEncoder(w).Encode(Response{
            Success: false,
            Message: "Validation failed",
            Errors:  fieldErrors,
        })
        return
    }

    // Симуляция успешной регистрации
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(Response{
        Success: true,
        Message: fmt.Sprintf("User %s registered successfully", req.Username),
    })
}

func main() {
    http.HandleFunc("/api/register", registerHandler)

    server := &http.Server{
        Addr:         ":8080",
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    fmt.Println("Server started on :8080")
    if err := server.ListenAndServe(); err != nil {
        fmt.Println("Server error:", err)
    }
}

Пример тестового запроса (через curl):

curl -X POST http://localhost:8080/api/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "john_doe",
    "email": "john@example.com",
    "password": "Str0ng!Pass",
    "confirm_password": "Str0ng!Pass",
    "age": 25,
    "phone": "+1234567890",
    "country": "US",
    "agreed_to_terms": true,
    "referral_code": "ABCD1234"
  }'

Ответ при успехе:

{"success":true,"message":"User john_doe registered successfully"}

Ответ при ошибке (например, несовпадение паролей):

Рекомендаций