Tarkibga o'tish

Request binding

Binding — so‘rov ma’lumotlarini Go struct’iga avtomatik o‘tkazish va shu bilan birga ularni tekshirish. So‘rov tanasidagi JSON, URL’dagi query parametrlar, yo‘l parametrlari va sarlavhalar — barchasi struct taglari asosida o‘qiladi.

net/httpda bu ish bir necha qadamdan iborat edi: tanani o‘qish, query parametrlarni turga o‘tkazish va validatorni alohida chaqirish. Gin’da esa ularning barchasi bitta chaqiruvda bajariladi.

Birinchi misol

type CreateBookRequest struct {
    Title  string `json:"title" binding:"required,min=1,max=200"`
    Author string `json:"author" binding:"required"`
    Year   int    `json:"year" binding:"required,gte=1000,lte=2100"`
}

r.POST("/books", func(c *gin.Context) {
    var req CreateBookRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusCreated, req)
})

binding tagi go-playground/validator qoidalarini ishlatadi. Gin bu kutubxonani ichida o‘zi chaqiradi, lekin tag nomi validate emas, binding. Qoidalarning ma’nosi Validation darsida ko‘rilgan: required, min, max, email, oneof va boshqalar.

ShouldBindJSON ikki ishni bajaradi:

  1. So‘rov tanasidagi JSON’ni struct’ga o‘qiydi.
  2. binding taglari bo‘yicha qiymatlarni tekshiradi.

Ikkala bosqichdagi xato ham err orqali qaytadi.

Ma’lumot manbalari

Manba Tag Metod
JSON tana json:"..." c.ShouldBindJSON(&v)
Query parametrlar form:"..." c.ShouldBindQuery(&v)
Yo‘l parametrlari uri:"..." c.ShouldBindUri(&v)
Sarlavhalar header:"..." c.ShouldBindHeader(&v)
HTML forma form:"..." c.ShouldBind(&v)

Query parametrlar

type ListBooksQuery struct {
    Page    int    `form:"page,default=1" binding:"gte=1"`
    PerPage int    `form:"per_page,default=20" binding:"gte=1,lte=100"`
    Sort    string `form:"sort,default=created_at" binding:"oneof=title year created_at"`
    Author  string `form:"author"`
}

r.GET("/books", func(c *gin.Context) {
    var q ListBooksQuery
    if err := c.ShouldBindQuery(&q); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    // q.Page, q.PerPage allaqachon int turida va tekshirilgan
})

form tagidagi default=1 parametr berilmaganda standart qiymat qo‘yadi. strconv.Atoi va qo‘lda yoziladigan chegara tekshiruvlari endi kerak emas.

sort parametri oneof bilan cheklangan. Bu SQL injection darsida aytilgan ruxsat ro‘yxati tamoyilining amaldagi ko‘rinishi.

Yo‘l parametrlari

Gin’da yo‘l parametrlari regex bilan cheklanmaydi. Ularni binding orqali tekshirish qulay:

type BookURI struct {
    ID int64 `uri:"id" binding:"required,gt=0"`
}

r.GET("/books/:id", func(c *gin.Context) {
    var u BookURI
    if err := c.ShouldBindUri(&u); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "id musbat son bo‘lishi kerak"})
        return
    }
    // u.ID — int64
})

Sarlavhalar

type ClientHeaders struct {
    RequestID  string `header:"X-Request-ID"`
    AppVersion string `header:"X-App-Version" binding:"required"`
}

Bind va ShouldBind farqi

Gin’da har bir binding metodining ikki xil varianti bor:

ShouldBind... Bind...
Xatoni qaytaradi Xatoni qaytaradi va avtomatik 400 javob yozadi
Javobni siz o‘zingiz yozasiz Javob text/plain formatida, matni o‘zgartirilmaydi
// Bind: xato bo‘lsa Gin o‘zi 400 yozadi va c.Abort() chaqiradi
if err := c.BindJSON(&req); err != nil {
    return // bu yerda c.JSON yozsangiz, "headers were already written" ogohlantirishi chiqadi
}

API’da deyarli har doim ShouldBind... ishlatiladi. Chunki xato javobining formati yagona standartda bo‘lishi kerak, Bind esa o‘z formatida yozib yuboradi.

Xatolarni tushunarli qilish

err.Error() matni klient uchun qulay emas:

Key: 'CreateBookRequest.Title' Error:Field validation for 'Title' failed on the 'required' tag

Validatsiya xatolarini Validation darsidagi kabi maydon va xabar juftliklariga aylantiramiz. Buning uchun Gin ichidagi validator’ga murojaat qilish kerak:

import (
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
)

func init() {
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterTagNameFunc(func(f reflect.StructField) string {
            for _, tag := range []string{"json", "form", "uri", "header"} {
                if name := strings.SplitN(f.Tag.Get(tag), ",", 2)[0]; name != "" && name != "-" {
                    return name
                }
            }
            return f.Name
        })
    }
}

binding.Validator.Engine() Gin ishlatadigan *validator.Validate obyektini qaytaradi. Unga maxsus qoidalar va maydon nomi funksiyasini qo‘shish mumkin. RegisterTagNameFunc json, form, uri va header taglaridan birinchi topilganini oladi. Shunda xatolarda klientga tanish nomlar chiqadi.

Binding xatosi ikki xil bo‘ladi: JSON formati buzilgan yoki validatsiya o‘tmagan. Ularni ajratib qayta ishlaymiz:

func bindError(c *gin.Context, err error) {
    var verrs validator.ValidationErrors
    if errors.As(err, &verrs) {
        fields := make(map[string]string, len(verrs))
        for _, fe := range verrs {
            fields[fe.Field()] = message(fe)
        }
        c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
            "error": gin.H{"code": "validation_failed", "message": "Ma’lumotlar noto‘g‘ri", "fields": fields},
        })
        return
    }

    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
        "error": gin.H{"code": "bad_request", "message": "So‘rov formati noto‘g‘ri"},
    })
}

Qat’iy JSON o‘qish

net/http bo‘limida notanish maydonlar DisallowUnknownFields bilan rad etilgan edi. Gin’da buning uchun global sozlama bor:

binding.EnableDecoderDisallowUnknownFields = true

Tana hajmini cheklash esa Gin’da ham http.MaxBytesReader bilan qilinadi. Uni middleware ko‘rinishida yozish qulay:

func maxBody(n int64) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, n)
        c.Next()
    }
}

To‘liq misol

main.go
package main

import (
    "errors"
    "fmt"
    "net/http"
    "reflect"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
)

// ---------- So‘rov turlari ----------

type CreateBookRequest struct {
    Title  string   `json:"title" binding:"required,min=1,max=200"`
    Author string   `json:"author" binding:"required"`
    Year   int      `json:"year" binding:"required,gte=1000,lte=2100"`
    Tags   []string `json:"tags" binding:"max=5,dive,min=2"`
}

type ListBooksQuery struct {
    Page    int    `form:"page,default=1" binding:"gte=1"`
    PerPage int    `form:"per_page,default=20" binding:"gte=1,lte=100"`
    Sort    string `form:"sort,default=created_at" binding:"oneof=title year created_at"`
}

type BookURI struct {
    ID int64 `uri:"id" binding:"required,gt=0"`
}

// ---------- Validator sozlamalari ----------

func setupValidator() {
    binding.EnableDecoderDisallowUnknownFields = true

    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterTagNameFunc(func(f reflect.StructField) string {
            for _, tag := range []string{"json", "form", "uri", "header"} {
                if name := strings.SplitN(f.Tag.Get(tag), ",", 2)[0]; name != "" && name != "-" {
                    return name
                }
            }
            return f.Name
        })
    }
}

func message(fe validator.FieldError) string {
    switch fe.Tag() {
    case "required":
        return "majburiy maydon"
    case "min":
        return fmt.Sprintf("kamida %s", fe.Param())
    case "max":
        return fmt.Sprintf("ko‘pi bilan %s", fe.Param())
    case "gte":
        return fmt.Sprintf("%s dan kichik bo‘lmasligi kerak", fe.Param())
    case "lte":
        return fmt.Sprintf("%s dan katta bo‘lmasligi kerak", fe.Param())
    case "gt":
        return fmt.Sprintf("%s dan katta bo‘lishi kerak", fe.Param())
    case "oneof":
        return "ruxsat etilgan qiymatlar: " + fe.Param()
    default:
        return "qiymat noto‘g‘ri"
    }
}

func bindError(c *gin.Context, err error) {
    var verrs validator.ValidationErrors
    if errors.As(err, &verrs) {
        fields := make(map[string]string, len(verrs))
        for _, fe := range verrs {
            fields[fe.Field()] = message(fe)
        }
        c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
            "error": gin.H{"code": "validation_failed", "message": "Ma’lumotlar noto‘g‘ri", "fields": fields},
        })
        return
    }

    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
        "error": gin.H{"code": "bad_request", "message": "So‘rov formati noto‘g‘ri"},
    })
}

// ---------- Handlerlar ----------

func createBook(c *gin.Context) {
    var req CreateBookRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        bindError(c, err)
        return
    }
    c.JSON(http.StatusCreated, gin.H{"data": req})
}

func listBooks(c *gin.Context) {
    var q ListBooksQuery
    if err := c.ShouldBindQuery(&q); err != nil {
        bindError(c, err)
        return
    }
    c.JSON(http.StatusOK, gin.H{"query": q})
}

func getBook(c *gin.Context) {
    var u BookURI
    if err := c.ShouldBindUri(&u); err != nil {
        bindError(c, err)
        return
    }
    c.JSON(http.StatusOK, gin.H{"data": gin.H{"id": u.ID}})
}

func main() {
    setupValidator()
    gin.SetMode(gin.ReleaseMode)

    r := gin.New()
    r.Use(gin.Recovery())
    r.POST("/books", createBook)
    r.GET("/books", listBooks)
    r.GET("/books/:id", getBook)

    r.Run(":8080")
}

Sinab ko‘ramiz:

curl -X POST localhost:8080/books -d '{"title":"Sarob","author":"Abdulla Qahhor","year":1937,"tags":["roman"]}'
curl -X POST localhost:8080/books -d '{"title":"","year":3000,"tags":["a"]}'
curl -X POST localhost:8080/books -d '{"title":"X","author":"Y","year":2000,"titel":"Z"}'
curl -X POST localhost:8080/books -d '{"title": "Sarob"'
curl "localhost:8080/books"
curl "localhost:8080/books?page=2&per_page=500&sort=price"
curl localhost:8080/books/42
curl localhost:8080/books/abc
{"data":{"title":"Sarob","author":"Abdulla Qahhor","year":1937,"tags":["roman"]}}
{"error":{"code":"validation_failed","fields":{"author":"majburiy maydon","tags[0]":"kamida 2","title":"majburiy maydon","year":"2100 dan katta bo‘lmasligi kerak"},"message":"Ma’lumotlar noto‘g‘ri"}}
{"error":{"code":"bad_request","message":"So‘rov formati noto‘g‘ri"}}
{"error":{"code":"bad_request","message":"So‘rov formati noto‘g‘ri"}}
{"query":{"Page":1,"PerPage":20,"Sort":"created_at"}}
{"error":{"code":"validation_failed","fields":{"per_page":"100 dan katta bo‘lmasligi kerak","sort":"ruxsat etilgan qiymatlar: title year created_at"},"message":"Ma’lumotlar noto‘g‘ri"}}
{"data":{"id":42}}
{"error":{"code":"bad_request","message":"So‘rov formati noto‘g‘ri"}}

Natijalar:

  • Validatsiya xatolari JSON maydon nomlari bilan va bir vaqtda qaytdi.
  • Notanish titel maydoni EnableDecoderDisallowUnknownFields tufayli rad etildi.
  • Query parametrlar berilmaganda default qiymatlari qo‘yildi.
  • /books/abc so‘rovida abc qiymatini int64ga o‘tkazib bo‘lmadi. Bu validatsiya emas, tur o‘zgartirish xatosi, shuning uchun 400 qaytdi.

ListBooksQuery javobida maydonlar Page, PerPage ko‘rinishida chiqdi, chunki ularda json tagi yo‘q. Bu misolda so‘rov struct’i to‘g‘ridan-to‘g‘ri javobga yozildi. Real loyihada javob uchun alohida DTO ishlatiladi.

Xulosa

  • ShouldBindJSON, ShouldBindQuery, ShouldBindUri va ShouldBindHeader so‘rov ma’lumotini struct’ga o‘qiydi va binding taglari bo‘yicha tekshiradi.
  • Manba tag orqali ko‘rsatiladi: json, form, uri, header. form tagida default qiymat berish mumkin.
  • API’da Bind... emas, ShouldBind... ishlatiladi. Javob formatini o‘zingiz boshqarasiz.
  • binding.Validator.Engine() orqali Gin validatoriga maydon nomlari va maxsus qoidalar qo‘shiladi.
  • Validatsiya xatolari (422) va format xatolari (400) alohida qayta ishlanadi.
  • EnableDecoderDisallowUnknownFields notanish maydonlarni rad etadi, tana hajmi esa MaxBytesReader bilan cheklanadi.

Keyingi darsda Gin’da JWT middleware yozamiz.