Контекст: Во всех handlers (places.go, services.go, auth.go и др.) валидация делается вручную через if req.Field == "" проверки. Нет единой схемы валидации, нет структурированных ошибок.
Ручная валидация — не масштабируема и ошибочна:
// backend/internal/handlers/places.go:120-147
func (h *PlaceHandler) Create(w http.ResponseWriter, r *http.Request) {
var req createPlaceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Title == "" { // Ручная проверка
writeError(w, http.StatusBadRequest, "title is required")
return
}
if req.Type == "" { // Ручная проверка
writeError(w, http.StatusBadRequest, "type is required")
return
}
// ... нет валидации: lat/lng range, URL формат, enum для type, длина строк
}
Стандарт де-факто в Go экосистеме. Поддерживает:
validate:"required,email,max=255"// internal/validator/validator.go
package validator
import (
"github.com/go-playground/validator/v10"
)
var Validate *validator.Validate
func init() {
Validate = validator.New()
// Регистрация кастомных валидаторов
Validate.RegisterValidation("coordinate", validateCoordinate)
}
func validateCoordinate(fl validator.FieldLevel) bool {
lat := fl.Field().Float()
return lat >= -90 && lat <= 90
}
// internal/handlers/places.go — DTO с тегами
type createPlaceRequest struct {
Title string `json:"title" validate:"required,min=1,max=255"`
Description *string `json:"description" validate:"omitempty,max=5000"`
Address *string `json:"address" validate:"omitempty,max=500"`
Lat float64 `json:"lat" validate:"required,coordinate,min=-90,max=90"`
Lng float64 `json:"lng" validate:"required,coordinate,min=-180,max=180"`
Type string `json:"type" validate:"required,oneof=place studio"`
Tags []string `json:"tags" validate:"dive,required,alphanum"`
Features []string `json:"features" validate:"dive,required,alphanum"`
HourlyRate *int `json:"hourly_rate" validate:"omitempty,min=0"`
Currency string `json:"currency" validate:"omitempty,len=3,uppercase"`
MinHours int `json:"min_hours" validate:"min=0"`
}
// В handler:
func (h *PlaceHandler) Create(w http.ResponseWriter, r *http.Request) {
var req createPlaceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := validator.Validate.Struct(req); err != nil {
var ve validator.ValidationErrors
if errors.As(err, &ve) {
writeValidationError(w, ve) // Структурированный ответ
return
}
writeError(w, http.StatusBadRequest, err.Error())
return
}
// ...
}
{
"type": "https://api.photoplaces.ru/errors/validation-error",
"title": "Validation Failed",
"status": 422,
"detail": "One or more fields failed validation",
"errors": [
{ "field": "title", "message": "title is required" },
{ "field": "lat", "message": "lat must be between -90 and 90" }
]
}
| Библиотека | Плюсы | Минусы |
|---|---|---|
| go-playground/validator (рекомендую) | Стандарт, быстрый, теги, кастомные правила | Рефлексия (небольшой оверхед) |
| go-ozzo/ozzo-validation | Функциональный стиль, без рефлексии | Менее популярный |
| Ручная (текущее) | Нет зависимостей | Неподдерживаемо, баги |
Задача: Code review PhotoPlaces — отсутствие валидации входных данных
#backend #validation #go #best-practice #technical-debt