Tarkibga o'tish

Response struct formatlash

API’dan foydalanadigan dasturchi har bir endpointning javobini o‘qib, unga moslashadi. Bir endpoint kitobni {"id":1,...} ko‘rinishida, boshqasi {"book":{...}} ko‘rinishida, uchinchisi ro‘yxatni {"items":[...]}, to‘rtinchisi esa to‘g‘ridan-to‘g‘ri [...] ko‘rinishida qaytarsa, klient kodi har bir holat uchun alohida yoziladi. Bu noqulay va xatolarga olib keladi.

Yaxshi API’da javoblar izchil bo‘ladi: bir xil vaziyatda bir xil tuzilma qaytadi. Bu darsda javoblarni tartibli formatlash usullarini ko‘ramiz.

1. Bitta writeJSON funksiyasi

Barcha JSON javoblar bitta funksiya orqali yozilsa, format ham bitta joyda boshqariladi:

func writeJSON(w http.ResponseWriter, status int, data any) error {
    body, err := json.Marshal(data)
    if err != nil {
        return err
    }

    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(status)
    _, err = w.Write(append(body, '\n'))
    return err
}

Bu yerda json.NewEncoder(w).Encode emas, json.Marshal ishlatildi. Sababi — xato holati. Marshal JSON’ni avval xotirada to‘liq tayyorlaydi. Xato bo‘lsa, klientga hali hech narsa yuborilmagan bo‘ladi va handler 500 qaytara oladi. Encoder esa yozishni darhol boshlaydi. Yarim yo‘lda xato chiqsa, klient 200 status bilan buzilgan JSON oladi.

JSON’ga aylantirib bo‘lmaydigan qiymatlar kam uchraydi: kanal, funksiya yoki NaN kabi suzuvchi nuqtali son. Lekin ular uchraganda to‘g‘ri xato javobini qaytarish muhim.

2. Ichki model va javob modeli (DTO)

Database’dagi foydalanuvchi modeli taxminan shunday bo‘ladi:

type User struct {
    ID           int64
    Email        string
    PasswordHash string
    Role         string
    FailedLogins int
    CreatedAt    time.Time
    DeletedAt    *time.Time
}

Uni to‘g‘ridan-to‘g‘ri JSON’ga aylantirish xavfli. Javobga PasswordHash, FailedLogins kabi ichki maydonlar ham tushib qoladi. json:"-" tagi yordam beradi, lekin yangi maydon qo‘shilganda tag unutilishi mumkin. Bundan tashqari, ichki modeldagi har bir o‘zgarish API javobini ham o‘zgartirib yuboradi.

Ishonchliroq yo‘l — javob uchun alohida tur yaratish. Bunday tur DTO (Data Transfer Object) deb ataladi:

type UserResponse struct {
    ID        int64     `json:"id"`
    Email     string    `json:"email"`
    Role      string    `json:"role"`
    CreatedAt time.Time `json:"created_at"`
}

func NewUserResponse(u User) UserResponse {
    return UserResponse{
        ID:        u.ID,
        Email:     u.Email,
        Role:      u.Role,
        CreatedAt: u.CreatedAt,
    }
}

Endi javobga faqat aniq ko‘rsatilgan maydonlar tushadi. Userga yangi maxfiy maydon qo‘shilsa ham, u tasodifan tashqariga chiqmaydi. Ichki model va API shartnomasi bir-biridan mustaqil o‘zgaradi.

Tip

Qoida sifatida esda tuting: database modelini hech qachon to‘g‘ridan-to‘g‘ri klientga qaytarmang. Ro‘yxat qaytarilganda ham har bir element DTO’ga aylantiriladi.

3. Envelope: javobni o‘rash

Javob qanday ko‘rinishda bo‘lishi haqida ikki keng tarqalgan yondashuv bor.

To‘g‘ridan-to‘g‘ri — resursning o‘zi qaytariladi:

{"id": 1, "title": "Sarob"}

Envelope (konvert) — ma’lumot data kaliti ichiga o‘raladi:

{"data": {"id": 1, "title": "Sarob"}}

Envelope qo‘shimcha ma’lumot joylash uchun joy qoldiradi. Masalan, ro‘yxat javobida sahifalash ma’lumotini ma’lumotlarning o‘zidan ajratib berish mumkin:

{
  "data": [{"id": 1, "title": "Sarob"}, {"id": 2, "title": "Ufq"}],
  "meta": {"page": 1, "per_page": 20, "total": 42}
}

Ro‘yxat to‘g‘ridan-to‘g‘ri massiv sifatida qaytarilsa, keyinchalik unga meta qo‘shib bo‘lmaydi. Buning uchun API’ni buzadigan o‘zgarish qilish kerak bo‘ladi. Shu sababli ro‘yxatlarni doim obyekt ichida qaytarish tavsiya etiladi.

Bu darsda quyidagi qoidadan foydalanamiz:

  • muvaffaqiyatli javob — {"data": ...}, ro‘yxat bo‘lsa meta ham qo‘shiladi;
  • xato — {"error": {...}}. Uning tuzilishi keyingi darslardan birida ko‘riladi.

Go’da bu shunday ifodalanadi:

type envelope struct {
    Data any       `json:"data"`
    Meta *pageMeta `json:"meta,omitempty"`
}

type pageMeta struct {
    Page       int `json:"page"`
    PerPage    int `json:"per_page"`
    Total      int `json:"total"`
    TotalPages int `json:"total_pages"`
}

Meta ko‘rsatkich va omitempty bilan belgilangan. Bitta resurs qaytarilganda u nil bo‘ladi va JSON’ga yozilmaydi.

4. Status kodlarni to‘g‘ri tanlash

Holat Status Tana
Ma’lumot qaytarildi 200 OK resurs yoki ro‘yxat
Yangi resurs yaratildi 201 Created yaratilgan resurs + Location sarlavhasi
So‘rov qabul qilindi, keyinroq bajariladi 202 Accepted vazifa holati
Muvaffaqiyatli, qaytariladigan ma’lumot yo‘q 204 No Content bo‘sh

204 javobida tana bo‘lishi mumkin emas. Unga JSON yozishga urinmang:

w.WriteHeader(http.StatusNoContent)
return

To‘liq misol

main.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "time"
)

// ---------- Ichki model ----------

type Book struct {
    ID         int
    Title      string
    Author     string
    PriceTiyin int64 // narx tiyinda saqlanadi
    Stock      int
    SupplierID int // ichki ma’lumot, klientga ko‘rsatilmaydi
    CreatedAt  time.Time
}

// ---------- DTO ----------

type BookResponse struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Author    string    `json:"author"`
    Price     string    `json:"price"`
    InStock   bool      `json:"in_stock"`
    CreatedAt time.Time `json:"created_at"`
}

func toBookResponse(b Book) BookResponse {
    return BookResponse{
        ID:        b.ID,
        Title:     b.Title,
        Author:    b.Author,
        Price:     fmt.Sprintf("%d.%02d", b.PriceTiyin/100, b.PriceTiyin%100),
        InStock:   b.Stock > 0,
        CreatedAt: b.CreatedAt.UTC(),
    }
}

// ---------- Javob yordamchilari ----------

type envelope struct {
    Data any       `json:"data"`
    Meta *pageMeta `json:"meta,omitempty"`
}

type pageMeta struct {
    Page       int `json:"page"`
    PerPage    int `json:"per_page"`
    Total      int `json:"total"`
    TotalPages int `json:"total_pages"`
}

func writeJSON(w http.ResponseWriter, status int, data any) error {
    body, err := json.Marshal(data)
    if err != nil {
        return err
    }
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(status)
    _, err = w.Write(append(body, '\n'))
    return err
}

func respond(w http.ResponseWriter, status int, data any) {
    if err := writeJSON(w, status, envelope{Data: data}); err != nil {
        log.Printf("javob yozishda xato: %v", err)
        http.Error(w, "ichki server xatosi", http.StatusInternalServerError)
    }
}

func respondList(w http.ResponseWriter, items any, page, perPage, total int) {
    meta := &pageMeta{
        Page:       page,
        PerPage:    perPage,
        Total:      total,
        TotalPages: (total + perPage - 1) / perPage,
    }
    if err := writeJSON(w, http.StatusOK, envelope{Data: items, Meta: meta}); err != nil {
        log.Printf("javob yozishda xato: %v", err)
        http.Error(w, "ichki server xatosi", http.StatusInternalServerError)
    }
}

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

var books = []Book{
    {1, "O‘tkan kunlar", "Abdulla Qodiriy", 4500000, 12, 7, time.Date(2026, 1, 10, 9, 0, 0, 0, time.UTC)},
    {2, "Kecha va kunduz", "Cho‘lpon", 3950050, 0, 7, time.Date(2026, 2, 3, 12, 30, 0, 0, time.UTC)},
    {3, "Sarob", "Abdulla Qahhor", 4100000, 4, 9, time.Date(2026, 3, 21, 8, 15, 0, 0, time.UTC)},
}

func listBooks(w http.ResponseWriter, r *http.Request) {
    page, _ := strconv.Atoi(r.URL.Query().Get("page"))
    if page < 1 {
        page = 1
    }
    const perPage = 2

    start := min((page-1)*perPage, len(books))
    end := min(start+perPage, len(books))

    items := make([]BookResponse, 0, end-start)
    for _, b := range books[start:end] {
        items = append(items, toBookResponse(b))
    }

    respondList(w, items, page, perPage, len(books))
}

func getBook(w http.ResponseWriter, r *http.Request) {
    id, _ := strconv.Atoi(r.PathValue("id"))
    for _, b := range books {
        if b.ID == id {
            respond(w, http.StatusOK, toBookResponse(b))
            return
        }
    }
    http.Error(w, "kitob topilmadi", http.StatusNotFound)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /books", listBooks)
    mux.HandleFunc("GET /books/{id}", getBook)

    log.Println("Server: http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
curl http://localhost:8080/books/2
{"data":{"id":2,"title":"Kecha va kunduz","author":"Cho‘lpon","price":"39500.50","in_stock":false,"created_at":"2026-02-03T12:30:00Z"}}
curl "http://localhost:8080/books?page=2"
{"data":[{"id":3,"title":"Sarob","author":"Abdulla Qahhor","price":"41000.00","in_stock":true,"created_at":"2026-03-21T08:15:00Z"}],"meta":{"page":2,"per_page":2,"total":3,"total_pages":2}}
curl "http://localhost:8080/books?page=5"
{"data":[],"meta":{"page":5,"per_page":2,"total":3,"total_pages":2}}

Misolda e’tibor berishga arziydigan jihatlar:

  • Pul miqdori tiyinda, butun son sifatida saqlanadi. float64 bilan pul hisoblash yaxlitlash xatolariga olib keladi. Masalan, 0.1 + 0.2 natijasi 0.30000000000000004 bo‘ladi. Klientga esa narx aniq formatlangan satr sifatida yuboriladi.
  • Stock o‘rniga in_stock qaytarildi. Omborda aniq nechta kitob borligi ichki ma’lumot bo‘lishi mumkin. Klientga faqat kerakli xulosa beriladi.
  • SupplierID javobga tushmadi. DTO’da bu maydon yo‘q.
  • Mavjud bo‘lmagan sahifa uchun [] qaytdi, null emas. items make bilan yaratilgani uchun u hech qachon nil bo‘lmaydi.
  • total_pages klientga sahifalash tugmalarini chizish imkonini beradi.

Chiroyli formatlangan JSON

Ishlab chiqish paytida JSON’ni o‘qish oson bo‘lishi uchun chekinish bilan yozish mumkin:

body, err := json.MarshalIndent(data, "", "  ")

Production’da bu keraksiz baytlar qo‘shadi. Terminalda chiroyli ko‘rish uchun esa jq yordamchi dasturi qulay:

curl -s http://localhost:8080/books/1 | jq

Xulosa

  • Barcha javoblarni bitta writeJSON funksiyasi orqali yozing. json.Marshal xatoni javob yuborilishidan oldin aniqlash imkonini beradi.
  • Ichki modelni to‘g‘ridan-to‘g‘ri qaytarmang. Javob uchun alohida DTO yarating.
  • Ro‘yxatlarni obyekt ichida qaytaring. Shunda keyinchalik meta qo‘shish mumkin bo‘ladi.
  • Ro‘yxat bo‘sh bo‘lsa, null emas, [] qaytaring.
  • Pulni butun sonda saqlang va formatlangan holda yuboring.
  • 204 javobida tana bo‘lmaydi.

Keyingi darsda so‘rov ma’lumotlarini qo‘lda yozilgan iflar o‘rniga go-playground/validator kutubxonasi bilan tekshirishni o‘rganamiz.