Deyarli har bir Go servisi ertami-kechmi struct'ni baytlarga va baytlarni qayta struct'ga aylantirishi kerak bo'ladi — HTTP API uchun, xabarlar navbati (message queue) uchun, kesh yozuvlari yoki konfiguratsiya fayllari uchun. encoding/json paketi bu ehtiyojlarning katta qismini to'liq qoplaydi va u shunchalik qulayki, uchinchi tomon JSON kutubxonalariga murojaat qilish kamdan-kam holatlarda oqlanadi. Biroq funksiya signaturalari ortida yashiringan nozik jihatlarni chuqur tushunish muhim: struct taglari ma'lumotlarni qanday boshqaradi, qachon maxsus marshaling yozish kerak va qaysi holatlarda JSON umuman to'g'ri tanlov bo'lmay qoladi?
json.Marshal va json.Unmarshal funksiyalari struct maydonlarini aylanib chiqish uchun reflection'dan foydalanadi. Struct taglari esa aynan shu reflection mexanizmiga har bir maydon bilan qanday ishlash kerakligini ko'rsatib beradi:
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"-"` // bu maydonni hech qachon marshal qilma
Email string `json:"email,omitempty"` // nol qiymat bo'lsa tushirib qoldir
Tags []string `json:"tags,omitempty"`
}
json:"-" — maxfiy qiymat JSON orqali tizimdan tashqariga chiqib ketmasligini maydon darajasida qat'iy kafolatlash usulidir: parol xeshi, ichki token yoki API javobida mutlaqo ko'rinmasligi kerak bo'lgan har qanday maxfiy ma'lumot. Bu "biz bu maydonni jo'natmaslikka ehtiyot bo'lamiz" degan va'dadan ancha kuchliroq va ishonchliroq himoya hisoblanadi, chunki kimdir keyinchalik e'tiborsizlik bilan butun struct'ni json.Marshal qilgan taqdirda ham, ushbu maydon baribir JSON'ga chiqmaydi.
omitempty — "bo'sh bo'lsa tushirib qoldir" degan ibora ko'rinishidan ko'ra nozikroq ishlaydi: u maydon qiymati o'z tipi uchun aynan nol qiymat (zero value) bo'lgandagina uni tushirib qoldiradi — sonlar uchun 0, satrlar uchun "", slice/map/pointer'lar uchun nil, bool'lar uchun false. Demak, omitempty qo'yilganda, ataylab kiritilgan Age: 0 va umuman kiritilmagan Age maydoni bir-biridan farqlanmaydi: ikkala holatda ham JSON'da maydon tushib qoladi. "Maydon nol qiymatga teng" va "maydon umuman berilmadi" holatlarini ajratish zarur bo'lsa, yechim — pointer ishlatishdir:
type UserUpdate struct {
Name *string `json:"name,omitempty"` // nil = berilmagan; nil bo'lmagan, hatto "" ham = berilgan
Age *int `json:"age,omitempty"` // nil = berilmagan; nil bo'lmagan 0 = aniq nol
}
Ixtiyoriy maydonlar uchun pointer ishlatish — PATCH uslubidagi qisman yangilash API'larida "mijoz bu maydonni o'zgartirmoqchi emas" bilan "mijoz uni aynan bo'shatmoqchi yoki 0 qilmoqchi" holatlarini aniq ajratib olish imkonini beradi. Go tilida alohida Optional[T] tipi bo'lmagani sababli, nil bo'la oladigan pointer eng idiomatik standart vosita hisoblanadi.
json.Unmarshal kiruvchi JSON ichidagi struct'da mavjud bo'lmagan ortiqcha maydonlarni ataylab e'tiborsiz qoldiradi: u vaqt o'tishi bilan yangi maydonlar qo'shiladigan API'larda eski mijozlar dasturi sinib qolmasligi (forward compatibility) uchun shunday loyihalangan. Aksariyat hollarda bu juda to'g'ri xatti-harakatdir. Ammo mijoz xatosini yoki sxema nomuvofiqligini darhol tutib olish kerak bo'lsa, Unmarshal o'rniga DisallowUnknownFields metodiga ega Decoderdan foydalanish lozim:
dec := json.NewDecoder(strings.NewReader(body))
dec.DisallowUnknownFields()
var req CreateUserRequest
if err := dec.Decode(&req); err != nil {
return fmt.Errorf("invalid request: %w", err)
}
Bu backend serveringiz qabul qiladigan kiruvchi so'rovlar (request body) uchun a'lo darajadagi xavfsizlik chorasidir: mijoz kutilmagan yoki noto'g'ri yozilgan maydonlarni yubora boshlasa, server buni jimgina o'tkazib yubormasdan, xatolikni darhol qaytaradi. Biroq uchinchi tomon tashqi API'laridan ma'lumot o'qiyotganda buni ishlatmaslik kerak, chunki ularga yangi maydonlar qo'shilishi tabiiy va bu sizning servisingizni to'xtatib qo'ymasligi lozim.
Ko'pchilik tiplar hech qanday qo'shimcha moslashtirishni talab qilmaydi: struct taglari yetarli bo'ladi. MarshalJSON/UnmarshalJSON metodlariga faqat tipning JSON ko'rinishi uning Go xotirasidagi ichki ko'rinishidan tubdan farq qilishi kerak bo'lgandagina murojaat qiling:
type Duration time.Duration
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).Seconds())
}
func (d *Duration) UnmarshalJSON(data []byte) error {
var seconds float64
if err := json.Unmarshal(data, &seconds); err != nil {
return err
}
*d = Duration(time.Duration(seconds * float64(time.Second)))
return nil
}
Barcha maydonlari uchun odatiy xatti-harakat talab qilinadigan struct'ga qo'shimcha validatsiya uchun maxsus UnmarshalJSON yozishda keng tarqalgan xavfli tuzoq bor: o'zining UnmarshalJSON metodi ichida receiver'ning o'zini yana json.Unmarshalga berish cheksiz rekursiyaga va dasturning qulashiga (stack overflow) olib keladi. Standart va to'g'ri yechim — funksiya ichida lokal type alias yaratishdir; bu alias aynan bir xil maydonlarga ega bo'ladi, ammo unda UnmarshalJSON metodi bo'lmaydi:
func (p *Person) UnmarshalJSON(data []byte) error {
type Alias Person // bir xil maydonlar, lekin UnmarshalJSON metodi yo'q — rekursiyani uzadi
aux := &struct{ *Alias }{Alias: (*Alias)(p)}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
if p.Age < 0 || p.Age > 150 {
return fmt.Errorf("invalid age: %d", p.Age)
}
return nil
}
Bu andoza — alias yaratish, unga decode qilish va so'ngra validatsiya amallarini bajarish — Go hamjamiyatida keng tarqalgan standart uslubdir.
Agar kiruvchi maydonning tipi noaniq bo'lsa (masalan, tashqi xizmat sonni goh 42, goh "42" ko'rinishida yuborsa), quyidagicha moslashuvchan unmarshaling yozish mumkin:
func (fn *FlexibleNumber) UnmarshalJSON(data []byte) error {
var num float64
if err := json.Unmarshal(data, &num); err == nil {
fn.Value = num
return nil
}
var str string
if err := json.Unmarshal(data, &str); err == nil {
parsed, err := strconv.ParseFloat(str, 64)
if err != nil {
return err
}
fn.Value = parsed
return nil
}
return fmt.Errorf("cannot unmarshal %s as number or string", data)
}
json.RawMessage esa "bu maydonning tipi o'zgaradi" degani emas, balki "payload'ning bu qismini decode qilishni keyinroqqa qoldirmoqchiman" degan vaziyatlar uchun mo'ljallangan. Masalan, JSON-RPC protokollarida tashqi konvert aniq, ammo ichki params qismi method nomiga qarab turlicha bo'ladi:
type Request struct {
Method string `json:"method"`
Params json.RawMessage `json:"params"` // Method aniqlangach, keyinroq kerakli tipga decode qilinadi
ID int `json:"id"`
}
Odatiy MarshalJSON har chaqirilganda yangi ajratilgan []byte slice'ini qaytaradi. Bu odatiy holatlarda yaxshi, ammo yuqori yuklamali tezkor nuqtalarda (hot path) bir xil turdagi qiymatni millionlab marta serializatsiya qilayotganda ortiqcha xotira sarfiga sabab bo'ladi. Go 1.24 versiyasida klassik encoding.TextMarshaler/encoding.BinaryMarshaler yoniga ikkita yangi interfeys qo'shildi: encoding.TextAppender va encoding.BinaryAppender. Ular yangi slice yaratish o'rniga, tayyor mavjud slice oxiriga o'z ma'lumotlarini qo'shib beradi (append qiladi):
type TextAppender interface {
AppendText(b []byte) ([]byte, error)
}
Go standart kutubxonasidagi ko'plab tiplar (time.Time, net/netip manzil tiplari, hash funksiyalari) ushbu interfeysni qo'llab-quvvatlaydi. Shuni aniq bilib qo'ying: Appender umumiy holatda MarshalJSONning o'rnini bosmaydi — encoding/json bugungi holatda AppendTextni o'zi chaqirmaydi. Ammo agar siz katta hajmlarda serializatsiya qilinadigan va maqsad buferni chaqiruvchi nazorat qiladigan tip yozayotgan bo'lsangiz, klassik Marshaler yoniga Appender variantini ham qo'shish — arzon va real unumdorlik yutug'i.
Har bir servisda identifikatorlarga (UUID) ehtiyoj tug'iladi — so'rov ID'lari (requestID), birlamchi kalitlar, idempotency tokenlari. Go 1.27 versiyasigacha dasturchilar deyarli doim tashqi github.com/google/uuid kutubxonasiga tayanishga majbur edilar. Go 1.27 standart kutubxonaga rasmiy uuid paketini kiritdi. Bu tashqi bog'liqliksiz v4 tasodifiy UUID yaratish, tahlil qilish va satrga aylantirish imkonini beradi.
json.Marshal/json.Unmarshal butun ma'lumotlar to'plami allaqachon xotirada ([]byte) bo'lishini talab qiladi. Bu kichik so'rovlar uchun to'g'ri kelsa-da, katta hajmdagi yoki oqimli ma'lumotlar uchun mos emas. json.NewEncoder(w) va json.NewDecoder(r) esa to'g'ridan-to'g'ri io.Writer va io.Reader bilan oqim tarzida ishlaydi:
enc := json.NewEncoder(w)
for user := range users { // users — channel, ma'lumot oqim bo'lib keladi
if err := enc.Encode(user); err != nil {
return err
}
}
Bu katta ma'lumotlar to'plamini yoki JSONL (newline-delimited JSON) formatidagi yirik log fayllarni butun xotiraga yuklab olmasdan, qadamma-qadam qayta ishlashning eng to'g'ri yo'lidir:
dec := json.NewDecoder(f)
for dec.More() {
var record LogEntry
if err := dec.Decode(&record); err != nil {
return err
}
process(record)
}
Go 1.25 versiyasida eksperimental tarzda to'liq qayta ko'rib chiqilgan yangi arxitektura taqdim etildi: encoding/json/jsontext (quyi darajadagi tokenlar oqimi) va uning ustiga qurilgan encoding/json/v2. Ushbu yangi versiya unumdorlikni sezilarli darajada oshiradi, xotira allokatsiyalarini kamaytiradi va qat'iyroq tekshiruvlarni joriy qiladi (noto'g'ri UTF-8 yoki takrorlangan JSON kalitlari sukut bo'yicha rad etiladi). Go 1.27 holatiga ko'ra, bu funksiyalar GOEXPERIMENT=jsonv2 orqasida ixtiyoriy sinov holatidadir. Klassik encoding/json esa o'z barqarorligini to'liq saqlab qoladi.
Go 1.27 v2 paketini kutilgan to'liq API bilan to'ldirdi: Marshal, MarshalWrite, MarshalEncode, Unmarshal, UnmarshalRead va UnmarshalDecode — ularning har biri xatti-harakatni boshqarish uchun Options ro'yxatini (variadic) qabul qiladi. Shu bilan birga 1.25 dagi dastlabki nomlar ham tartibga solindi: ichki struct maydonlarini bir sathga chiqarish (inline qilish) uchun struct tag endi inline emas, embed deb yoziladi.
JSON qulay, inson o'qiy oladigan va universal formatdir, ammo u barcha vaziyatlar uchun yagona yechim emas.
encoding/xml — eski yoki korporativ XML tizimlari bilan integratsiyalashuv uchun mavjud bo'lib, xuddi JSON kabi xml:"..." struct taglari bilan ishlaydi.encoding/gob — faqat Go dasturlarining o'zaro muloqoti uchun mo'ljallangan binar format bo'lib, begona tillarda o'qib bo'lmaydi, biroq Go klasterlari ichida sxemalarsiz juda tez va samarali ishlaydi.google.golang.org/protobuf) — microservice'lar va turli dasturlash tillari orasida yuqori tezlik, minimal trafik va qat'iy versiyalangan sxemalar talab etilganda eng yaxshi tanlovdir. Ichki yuqori yuklamali tarmoqlarda JSON o'rniga Protobuf ishlatish CPU va tarmoq resurslarini bir necha baravar tejaydi. Buning evaziga qo'shimcha asbob-uskuna paydo bo'ladi: alohida .proto fayl va kod generatsiyasi bosqichi. Shu sababli ommaviy HTTP API uchun JSON'ning o'qilishi va universal mijoz qo'llab-quvvatlashi ko'pincha — hatto o'lcham va parse tezligi hisobiga bo'lsa ham — ustun keladi. Binar formatga trafik ichki, yuqori hajmli yoki kechikish profilida farq haqiqatan ko'rinadigan darajada sezgir bo'lgandagina o'ting.Eng muhim xulosa: qaysi formatni tanlashingizdan qat'i nazar, har qanday decoder'dan chiqqan ma'lumotni kodingizda albatta validatsiya qiling. Unmarshalning muvaffaqiyatli yakunlanishi faqat sintaktik shakl to'g'ri ekanini bildiradi; -5 yosh yoki bo'sh Username sizning biznes mantig'ingizga to'g'ri kelish-kelmasligini faqat sizning kodingiz tekshira oladi.
Unmarshalning muvaffaqiyatli bo'lishi faqat sintaktik shakl to'g'riligini bildiradi — qiymatlarning biznes logikangiz uchun to'g'ri ekanligini validatsiya qilish sizning kodingizning vazifasidir.
Serializatsiya asoslarini ko'rib chiqdik. Keyingi bobda ushbu baytlar tarmoq orqali qanday harakatlanishini o'rganamiz: net/http paketi, client va server arxitekturasi hamda middleware mexanizmlari.