Tarkibga o'tish

encoding/json asoslari

API klient bilan JSON orqali gaplashadi. So‘rov JSON bo‘lib keladi, javob JSON bo‘lib ketadi. Go’da bu ishni standart kutubxonadagi encoding/json paketi bajaradi.

Paketning asosiy imkoniyatlari — Marshal, Unmarshal, struct taglar, Encoder va Decoder — Go’da JSON bilan ishlash darsida ko‘rib chiqilgan. Bu dars o‘sha bilimlarni API nuqtai nazaridan davom ettiradi. API yozishda tez-tez uchraydigan savollarga javob beramiz: maydon yuborilmaganini nulldan qanday ajratamiz? Vaqt qanday formatda ketadi? Katta ID’lar JavaScript’da nega buziladi?

Qisqa takrorlash

main.go
package main

import (
    "encoding/json"
    "fmt"
)

type Book struct {
    ID       int      `json:"id"`
    Title    string   `json:"title"`
    Tags     []string `json:"tags,omitempty"`
    Internal string   `json:"-"`
}

func main() {
    b := Book{ID: 1, Title: "Sarob", Internal: "maxfiy"}

    data, _ := json.Marshal(b)
    fmt.Println(string(data))

    var b2 Book
    json.Unmarshal([]byte(`{"id":2,"title":"Ufq","tags":["roman"]}`), &b2)
    fmt.Printf("%+v\n", b2)
}
{"id":1,"title":"Sarob"}
{ID:2 Title:Ufq Tags:[roman] Internal:}
  • json:"title" — JSON’dagi kalit nomi.
  • omitempty — qiymat bo‘sh bo‘lsa, maydon JSON’ga yozilmaydi.
  • json:"-" — maydon JSON’da umuman ishtirok etmaydi.
  • Kichik harf bilan boshlangan (eksport qilinmagan) maydonlar ham JSON’ga tushmaydi.

API’da ikkinchi va uchinchi qoida xavfsizlik uchun muhim. Masalan, foydalanuvchi strukturasidagi PasswordHash maydoni json:"-" bilan belgilanmasa, parol xeshi javobda klientga ketib qolishi mumkin.

Nomlash uslubi

JSON kalitlari uchun ikki uslub keng tarqalgan: snake_case (created_at) va camelCase (createdAt). Go’ning o‘zi hech qaysisini majburlamaydi. Muhimi — butun API bo‘ylab bitta uslubni izchil ishlatish.

Tag yozilmasa, Go maydon nomining o‘zini ishlatadi: CreatedAt → "CreatedAt". API’da bu kamdan-kam ma’qul keladi, shuning uchun har bir maydonga tag yozing.

Ixtiyoriy maydonlar: yo‘q, null va nol qiymat

Bu API’dagi eng ko‘p chalkashlik tug‘diradigan mavzu. Klient kitobni qisman yangilash uchun PATCH /books/7 so‘rovini yuboradi:

{"year": 0}

Server bu so‘rovni qanday tushunishi kerak? "Yilni 0 qil" demoqchimi yoki klient yilni umuman yubormaganmi? Oddiy int maydonda bu ikki holat bir xil ko‘rinadi, chunki ikkalasida ham qiymat 0.

Yechim — ko‘rsatkich (pointer) turlari:

type BookPatch struct {
    Title *string `json:"title"`
    Year  *int    `json:"year"`
}
Kelgan JSON Year qiymati
{} nil — maydon yuborilmagan
{"year": null} nil — maydon null
{"year": 0} 0 ga ko‘rsatkich
{"year": 1937} 1937 ga ko‘rsatkich

Misol

main.go
package main

import (
    "encoding/json"
    "fmt"
)

type Book struct {
    Title string
    Year  int
}

type BookPatch struct {
    Title *string `json:"title"`
    Year  *int    `json:"year"`
}

func (p BookPatch) Apply(b *Book) {
    if p.Title != nil {
        b.Title = *p.Title
    }
    if p.Year != nil {
        b.Year = *p.Year
    }
}

func main() {
    book := Book{Title: "Sarob", Year: 1937}

    var patch BookPatch
    json.Unmarshal([]byte(`{"title":"Sarob (yangi nashr)"}`), &patch)
    patch.Apply(&book)

    fmt.Printf("%+v\n", book)
}
{Title:Sarob (yangi nashr) Year:1937}

Klient faqat title yubordi. Year nil bo‘lib qoldi va kitob yili o‘zgarmadi.

Info

Standart encoding/json "maydon yuborilmagan" va "null yuborilgan" holatlarini ajratmaydi, ikkalasida ham nil bo‘ladi. Aksariyat API’lar uchun bu yetarli. Ajratish zarur bo‘lsa, maydonni json.RawMessage sifatida o‘qib, keyin qo‘lda tekshirish mumkin.

Javobda null va []

Javob yozishda ham shunga o‘xshash nozik jihat bor:

var a []string         // nil slice
b := []string{}        // bo‘sh slice

json.Marshal(a) // null
json.Marshal(b) // []

Klient ro‘yxat kutayotgan joyda null olsa, JavaScript kodi data.items.length qatorida xato beradi. Shuning uchun ro‘yxat qaytaradigan joylarda bo‘sh slice’ni aniq yarating:

books := make([]Book, 0)

Vaqt: time.Time

time.Time JSON’ga RFC 3339 formatida yoziladi:

type Event struct {
    At time.Time `json:"at"`
}

json.Marshal(Event{At: time.Date(2026, 9, 30, 14, 5, 0, 0, time.UTC)})
// {"at":"2026-09-30T14:05:00Z"}

Bu format xalqaro standart. Uni JavaScript, Python va boshqa tillar muammosiz tushunadi. API’da vaqtni shu formatda, UTC’da yuborish odatiy amaliyot. Mahalliy vaqtga o‘tkazish esa klient zimmasida.

Kiruvchi JSON’dagi vaqt ham shu formatda bo‘lishi kerak. "30.09.2026" kabi boshqa formatni time.Time qabul qilmaydi va xato qaytaradi.

Katta sonlar va string opsiyasi

JavaScript’da barcha sonlar 64 bitli suzuvchi nuqtali (float64) turda saqlanadi. U butun sonlarni faqat 2⁵³ gacha aniq ifodalay oladi. Go’dagi int64 esa bundan ancha katta bo‘lishi mumkin:

Go:          1234567890123456789
JavaScript:  1234567890123456800   ← oxirgi raqamlar yo‘qoldi

Katta ID’larni, masalan Snowflake ID’larini, JSON’da satr sifatida yuborish kerak. Buning uchun string tag opsiyasi bor:

type Order struct {
    ID int64 `json:"id,string"`
}

json.Marshal(Order{ID: 1234567890123456789})
// {"id":"1234567890123456789"}

Go tomonida maydon int64 bo‘lib qoladi, JSON’da esa qo‘shtirnoq ichida yoziladi. Unmarshal ham "id":"123" ko‘rinishidagi qiymatni to‘g‘ri o‘qiydi.

map[string]any va sonlar

Tuzilishi oldindan noma’lum JSON’ni map[string]anyga o‘qish mumkin. Lekin bunda barcha sonlar float64 bo‘lib keladi:

var m map[string]any
json.Unmarshal([]byte(`{"id": 42}`), &m)

fmt.Printf("%T\n", m["id"]) // float64

Bu katta sonlarda aniqlik yo‘qolishiga olib keladi. Decoderning UseNumber metodi sonlarni json.Number turida, ya’ni satr ko‘rinishida saqlaydi:

dec := json.NewDecoder(r.Body)
dec.UseNumber()
dec.Decode(&m)

n := m["id"].(json.Number)
id, err := n.Int64()

Umuman olganda, API’da map[string]any o‘rniga aniq struct’lardan foydalaning. Struct turi hujjat vazifasini ham bajaradi va xatolarni kamaytiradi.

json.RawMessage — keyinroq o‘qish

Ba’zan JSON’ning bir qismi turga qarab turlicha tuzilgan bo‘ladi. Masalan, webhook hodisalari:

{"type": "payment", "data": {"amount": 50000, "currency": "UZS"}}
{"type": "refund",  "data": {"payment_id": "p_123", "reason": "qaytarildi"}}

data maydonining tuzilishi typega bog‘liq. json.RawMessage bu qismni o‘qimasdan, xom baytlar holida saqlab turadi:

main.go
package main

import (
    "encoding/json"
    "fmt"
)

type Event struct {
    Type string          `json:"type"`
    Data json.RawMessage `json:"data"`
}

type Payment struct {
    Amount   int    `json:"amount"`
    Currency string `json:"currency"`
}

type Refund struct {
    PaymentID string `json:"payment_id"`
    Reason    string `json:"reason"`
}

func handle(raw []byte) error {
    var e Event
    if err := json.Unmarshal(raw, &e); err != nil {
        return err
    }

    switch e.Type {
    case "payment":
        var p Payment
        if err := json.Unmarshal(e.Data, &p); err != nil {
            return err
        }
        fmt.Printf("To‘lov: %d %s\n", p.Amount, p.Currency)
    case "refund":
        var r Refund
        if err := json.Unmarshal(e.Data, &r); err != nil {
            return err
        }
        fmt.Printf("Qaytarish: %s (%s)\n", r.PaymentID, r.Reason)
    default:
        return fmt.Errorf("noma’lum hodisa: %s", e.Type)
    }
    return nil
}

func main() {
    handle([]byte(`{"type":"payment","data":{"amount":50000,"currency":"UZS"}}`))
    handle([]byte(`{"type":"refund","data":{"payment_id":"p_123","reason":"qaytarildi"}}`))
}
To‘lov: 50000 UZS
Qaytarish: p_123 (qaytarildi)

Avval faqat type o‘qiladi. Keyin Data ichidagi xom JSON kerakli turga o‘qiladi.

Maxsus MarshalJSON va UnmarshalJSON

Tur json.Marshaler yoki json.Unmarshaler interfeysini amalga oshirsa, encoding/json standart qoidalar o‘rniga shu metodlarni chaqiradi. Bu, masalan, Go’dagi enum qiymatlarini JSON’da tushunarli satr ko‘rinishida yuborish uchun qulay:

main.go
package main

import (
    "encoding/json"
    "fmt"
)

type Status int

const (
    StatusDraft Status = iota
    StatusPublished
    StatusArchived
)

var statusNames = map[Status]string{
    StatusDraft:     "draft",
    StatusPublished: "published",
    StatusArchived:  "archived",
}

func (s Status) MarshalJSON() ([]byte, error) {
    name, ok := statusNames[s]
    if !ok {
        return nil, fmt.Errorf("noma’lum status: %d", s)
    }
    return json.Marshal(name)
}

func (s *Status) UnmarshalJSON(data []byte) error {
    var name string
    if err := json.Unmarshal(data, &name); err != nil {
        return fmt.Errorf("status satr bo‘lishi kerak")
    }
    for k, v := range statusNames {
        if v == name {
            *s = k
            return nil
        }
    }
    return fmt.Errorf("noma’lum status: %q", name)
}

type Article struct {
    Title  string `json:"title"`
    Status Status `json:"status"`
}

func main() {
    out, _ := json.Marshal(Article{Title: "Go haqida", Status: StatusPublished})
    fmt.Println(string(out))

    var a Article
    err := json.Unmarshal([]byte(`{"title":"X","status":"deleted"}`), &a)
    fmt.Println("Xato:", err)
}
{"title":"Go haqida","status":"published"}
Xato: noma’lum status: "deleted"

Go ichida status int sifatida saqlanadi va tez solishtiriladi. JSON’da esa klient tushunadigan "published" ko‘rinishida chiqadi. Noto‘g‘ri qiymat kelganda UnmarshalJSON xato qaytaradi va handler 400 javobini beradi.

Warning

MarshalJSON qiymat (value) qabul qiluvchiga, UnmarshalJSON esa ko‘rsatkich qabul qiluvchiga yoziladi. UnmarshalJSON qiymatni o‘zgartirishi kerak, shuning uchun unga ko‘rsatkich zarur. MarshalJSON ko‘rsatkichga yozilsa, struct qiymat sifatida marshal qilinganda metod chaqirilmay qolishi mumkin.

Marshal yoki Encoder?

Holat Tanlov
JSON’ni http.ResponseWriterga yozish json.NewEncoder(w).Encode(v)
So‘rov tanasini o‘qish json.NewDecoder(r.Body).Decode(&v)
JSON’ni baytlar sifatida olish (keshga, bazaga, logga) json.Marshal(v)
Xotiradagi baytlardan o‘qish json.Unmarshal(data, &v)

Encoder natijani to‘g‘ridan-to‘g‘ri oqimga yozadi va oxiriga \n qo‘shadi. Marshal esa butun natijani avval xotirada yig‘adi.

Bir farqni yodda tuting: Encode yozishni boshlagandan keyin xato yuz bersa, klientga yarim JSON va 200 status ketib qoladi. Marshal bilan avval baytlarni tayyorlab, keyin status yozilsa, xatoni 500 sifatida qaytarish mumkin. Bu yondashuv Response struct formatlash darsida qo‘llanadi.

HTML belgilari

Standartda encoding/json <, > va & belgilarini <, >, & ko‘rinishida kodlaydi. Bu JSON HTML sahifa ichiga joylansa, XSS hujumidan himoya qiladi. API uchun bu zararsiz: har qanday JSON parser ularni to‘g‘ri o‘qiydi. Kodlashni o‘chirish zarur bo‘lsa, enc.SetEscapeHTML(false) ishlatiladi.

Xulosa

  • Har bir maydonga JSON tag yozing va butun API’da bitta nomlash uslubini ishlating.
  • Maxfiy maydonlarni json:"-" bilan yashiring.
  • Ixtiyoriy maydonlar uchun ko‘rsatkich turlarini ishlating: nil — yuborilmagan, aks holda qiymat bor.
  • Ro‘yxat uchun nil emas, bo‘sh slice qaytaring: null o‘rniga [] chiqadi.
  • Vaqt RFC 3339 formatida yuboriladi. UTC ishlatish tavsiya etiladi.
  • Katta int64 ID’larni ,string opsiyasi bilan satr ko‘rinishida yuboring.
  • Tuzilishi turga bog‘liq qism uchun json.RawMessage, maxsus format uchun MarshalJSON va UnmarshalJSON ishlatiladi.

Keyingi darsda so‘rov tanasini xavfsiz va to‘g‘ri o‘qishni, jumladan xatolarni tushunarli xabarlarga aylantirishni o‘rganamiz.