Map — Goning o'rnatilgan xesh-jadvali (hash table): kalit-qiymat juftliklari, qidirish, qo'shish va o'chirish o'rtacha O(1) da bajariladi — va u slice'lardan keyin tildagi eng ko'p ishlatiladigan tiplardan biri. Shu bilan birga u aniq, qayta-qayta uchraydigan xatolar sinfining manbai ham: "bu kalit mavjud emas" bilan "bu kalit mavjud va tasodifan nolga teng" ni chalkashtirish. Maplar haqida bilish foydali bo'lgan deyarli hamma narsa yo shu farqni tushuntiradi, yo unga tayanadi.
Maplardagi eng keng tarqalgan xato — "kalit mavjud emas" bilan "kalit mavjud va nolga teng" ni chalkashtirish.
Mapga yozishdan oldin uni initsializatsiya qilish kerak — yo make bilan, yo literal bilan:
m := make(map[string]int)
m2 := map[string]int{
"apple": 5,
"banana": 3,
"cherry": 8,
}
m3 := map[string]int{} // bu ham yaroqli — bo'sh, lekin initsializatsiya qilingan
E'lon qilingan, lekin initsializatsiya qilinmagan map nil bo'ladi, va nil map o'qish va yozishda turlicha tutadi: undan erkin o'qish mumkin (har bir qidirish shunchaki nol qiymatni qaytaradi, xuddi map bo'sh bo'lgandek), lekin unga yozish panic'ga olib keladi.
var m map[string]int
value := m["key"] // yaxshi — 0 qaytaradi, nil map bo'sh deb qaraladi
m["key"] = 1 // panic: nil map'dagi elementga qiymat berish
Bu assimetriya ba'zan map'ni parametr sifatida qabul qilib, yozishdan oldin uning nil emasligini tekshirmaydigan kodda odamlarni qoqintiradi — map funksiya chegarasidan o'tganda va uni chaqiruvchi initsializatsiya qilganiga ishonchingiz komil bo'lmaganda, bir lahza ehtiyot bo'lish foydali.
nilmap'dan o'qish mumkin, lekin unga yozish panic'ga olib keladi.
Kalitni o'rnatish va yangilash bir xil sintaksisdan foydalanadi:
m := make(map[string]int)
m["apple"] = 5 // qo'shish
m["apple"] = 10 // yangilash — sintaksis bir xil, Go ularni ajratmaydi
Mavjud bo'lmagan kalitni o'qish panic yoki xatoga olib kelmaydi — u jimgina qiymat tipining nol qiymatini qaytaradi, noaniqlik aynan shu yerdan keladi:
m := map[string]int{"apple": 5}
v := m["apple"] // 5
v = m["banana"] // 0 — lekin bu banana 0 ga bog'langanligi uchunmi, yoki banana umuman yo'qligi uchunmi?
Mapdan o'qishning ikki qiymatli shakli — "comma-ok idiomasi" — noaniqlikni kalit haqiqatan ham mavjud bo'lganmi yoki yo'qmi, shuni bildiruvchi ikkinchi mantiqiy qiymatni qaytarish orqali hal qiladi:
value, ok := m["apple"]
if ok {
fmt.Println("topildi:", value)
}
if _, exists := m["banana"]; !exists {
fmt.Println("banana map'da yo'q")
}
Nol qiymat sizning map'ngiz qonuniy ravishda saqlashi mumkin bo'lgan qiymat bo'lgan har qanday holatda comma-ok'dan foydalaning — sonli va mantiqiy qiymat tiplari uchun bu deyarli har doim shunday. Uni o'tkazib yuborib, shunchaki if value == 0 deb tekshirish — qiymati 0 bo'lgan haqiqiy yozuv paydo bo'lgan kunni kutib turgan nozik xatodir.
Nol qiymat map'ngiz saqlashi mumkin bo'lgan qiymat bo'lsa, comma-ok'dan foydalaning.
delete(m, key) yozuvni o'chiradi va kalit mavjud bo'lmasa ham chaqirish doim xavfsiz — u xato emas, hech narsa qilmaydi, shuning uchun faqat o'chirish uchun oldindan mavjudligini tekshirish shart emas:
delete(m, "apple") // mavjud bo'lsa o'chiradi, aks holda hech narsa qilmaydi
Tushuncha jihatidan Go map'si xesh-jadval (hash table) hisoblanadi: u qutilar (buckets) massiviga ko'rsatkichni (pointer) saqlovchi kichik sarlavhadir. Har bir quti bir xil katakka tushgan bir nechta kalit-qiymat juftliklarini saqlaydi. m[key] qidiruvi avval key ning xeshini hisoblaydi, shu xesh yordamida tegishli qutini tanlaydi, so'ng quti ichidagi yozuvlar orasidan aniq mos keluvchi kalitni topadi. Mapdagi elementlar soni oshib borgan sari (load factor chegarasidan o'tganda), u xotiradan kattaroq qutilar massivini ajratadi va yozuvlarni yangi qutilarga qayta taqsimlaydi (rehash qiladi). Bu xuddi slice'larning append paytidagi o'sishiga o'xshaydi va shu sababli map amallari o'rtacha O(1) vaqt murakkabligiga ega.
Maplarning ichki tuzilishidan ikkita juda muhim amaliy xususiyat kelib chiqadi:
for range orqali map bo'yicha yurish tartibi har safar ataylab o'zgartirib turiladi. Bu qasddan qilingan xavfsizlik chorasi bo'lib, dasturchilar hech qachon kafolatlanmagan ketma-ketlikka tayanib qolmasliklari hamda xesh-jadval to'qnashuvi (hash collision DoS) hujumlarining oldini olish uchun joriy etilgan.Map bo'ylab yurish tartibi doimo tasodifiy bo'ladi — uning ketma-ketligi haqida hech qachon oldindan xulosa chiqarmang.
for k, v := range m {
process(k, v) // tartib tasodifiy — har safar har xil bo'lishi mumkin
}
Aniq tartib kerak bo'lsa, kalitlarni yig'ing, saralang va saralangan slice ustidan yuring:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
slices.Sort(keys) // slices paketi (Go 1.21+); sort.Strings(keys) ham hamon ishlaydi
for _, k := range keys {
fmt.Printf("%s: %d\n", k, m[k])
}
Map kalitlari taqqoslanadigan (comparable) bo'lishi kerak — == bilan ishlatib bo'ladigan — bu slice'lar, map'lar va funksiyalarni kalit tipi sifatida chiqarib tashlaydi, chunki ularning hech biri tenglik taqqoslashini qo'llab-quvvatlamaydi:
// bularning hech biri kompilyatsiya bo'lmaydi:
// map[[]int]string
// map[map[int]int]string
// map[func()]string
Asosiy tiplar, massivlar (agar ularning element tipi taqqoslanadigan bo'lsa), struct'lar (agar har bir maydoni taqqoslanadigan bo'lsa), pointer'lar va taqqoslanadigan dinamik tipni saqlovchi interface'lar — hammasi joiz; bu struct kalitlarini kompozit kalitlar uchun haqiqiy yaxshi variantga aylantiradi va maydonlarni satrga qo'shib yozishdan ko'ra odatda afzal qiladi:
type Point struct{ X, Y int }
grid := make(map[Point]string)
grid[Point{X: 1, Y: 2}] = "wall"
Bu fmt.Sprintf("%d-%d", x, y) kabi satr kalitini qurishdan ham aniqroq, ham arzonroq — u har bir qidirishda faqat tashlab yuborish uchun satr ajratadi. Qiymatlar esa, aksincha, mutlaqo har qanday narsa bo'lishi mumkin — jumladan boshqa map'lar yoki slice'lar, ichma-ich tuzilmalar shunday quriladi:
byCategory := make(map[string][]Product)
nested := make(map[string]map[string]int)
Goda o'rnatilgan to'plam tipi yo'q, lekin qiziq bo'lmagan qiymat tipiga ega map bu ishni bajaradi. Sodda variant map[T]bool dan foydalanadi:
seen := make(map[string]bool)
seen["apple"] = true
if seen["apple"] { /* ... */ }
bu ishlaydi, lekin satr va butun sonlardagi "u false'mi yoki yo'qmi" noaniqligini o'zida olib yuradi — va yanada nozikroq, mazmuni uchun emas, faqat mavjudligi uchun o'qiladigan qiymat tufayli har bir yozuvga bir bayt xotirani isrof qiladi. Idiomatik shakl map[T]struct{} dan foydalanadi, chunki bo'sh struct struct{}{} nol bayt egallaydi va a'zolik mantiqiy qiymatni o'qish bilan emas, comma-ok bilan tekshiriladi:
set := make(map[string]struct{})
set["apple"] = struct{}{}
if _, ok := set["apple"]; ok {
fmt.Println("apple to'plamda bor")
}
delete(set, "apple")
To'plam amallari to'g'ridan-to'g'ri map iteratsiyasidan kelib chiqadi:
func union(a, b map[string]struct{}) map[string]struct{} {
result := make(map[string]struct{}, len(a)+len(b))
for k := range a {
result[k] = struct{}{}
}
for k := range b {
result[k] = struct{}{}
}
return result
}
func intersection(a, b map[string]struct{}) map[string]struct{} {
result := make(map[string]struct{})
for k := range a {
if _, ok := b[k]; ok {
result[k] = struct{}{}
}
}
return result
}
Go 1.21 slices yoniga standart maps paketini qo'shdi; unda maps.Clone, maps.Equal va maps.DeleteFunc kabi yordamchilar bor — standart kutubxona endi qoplaydigan narsani qo'lda yozishdan oldin tekshirib ko'rish foydali, garchi yuqoridagi to'plam qurish andozalari yordamchi funksiyalarga murojaat qilganingizda ham to'g'ri aqliy model bo'lib qolsa-da.
maps.Keys va maps.Values yaqinroq qarash foydali, chunki ularning shakli muhim tarzda o'zgargan: Go 1.23 dan boshlab ikkalasi ham slice emas, iteratorlarni qaytaradi (mos ravishda iter.Seq[K] va iter.Seq[V]). Demak, for k := range maps.Keys(m) bevosita ishlaydi, lekin endi []K ni bepul olmaysiz — agar sizga haqiqatan slice kerak bo'lsa, uni slices.Collect bilan aniq to'plang:
for k := range maps.Keys(m) { // oraliq slice'iz kalitlar ustidan yuring
fmt.Println(k)
}
keys := slices.Collect(maps.Keys(m)) // kerak bo'lganda []K ni hosil qiling
Doim uchraydigan kombinatsiya — "menga shu map'ning kalitlarini, saralangan holda ber" — bir qadamda ham to'playdigan, ham saralaydigan slices.Sorted orqali to'g'ridan-to'g'ri zanjirlanadi:
for _, k := range slices.Sorted(maps.Keys(m)) {
fmt.Printf("%s: %v\n", k, m[k]) // aniq tartib, qo'lda saralash qadamisiz
}
Bu bir qator ushbu bobda avvalroq ko'rsatilgan barqaror, diff qilish qulay map chiqishini hosil qilish uchun "slice'ga yig', so'ng slices.Sort qil" andozasini almashtiradi. Paketni to'ldirish uchun: maps.All(m) kalitlar va qiymatlarni alohida emas, birgalikda kalit-qiymat juftliklari ustidan iteratorni qaytaradi (iter.Seq2[K, V]), maps.Insert(m, seq) / maps.Collect(seq) esa teskari yo'nalishda ishlaydi — Insert iteratorning kalit-qiymat juftliklarini mavjud map'ga qo'shadi, Collect esa undan butunlay yangi map quradi. Masalan, maps.Collect(maps.All(a)) map'ning bir xil kalit-qiymat juftliklari bilan sayoz nusxasini yozishning ixcham usuli, garchi "shunchaki bu map'ni nusxala" ning eng to'g'ridan-to'g'ri yozilishi maps.Clone bo'lib qolsa ham.
Hodisalarni sanash map'ning eng tez-tez ishlatiladigan vazifalaridan biri, va u ko'rishdayoq idiomatik bo'ladigan darajada qisqa:
counts := make(map[string]int)
for _, item := range items {
counts[item]++ // mavjud bo'lmagan kalitni o'qish 0 beradi, shuning uchun bu birinchi uchrashuvdan "shunchaki ishlaydi"
}
Oxirgi qator to'g'ridan-to'g'ri avval muhokama qilingan nol qiymat xatti-harakatiga tayanadi — mutlaqo yangi kalitdagi counts[item]++ 0 ni o'qiydi, 1 qo'shadi va 1 ni qaytarib yozadi, aniq initsializatsiya kerak emas. Xuddi shu g'oya guruhlashga ham kengayadi:
groups := make(map[string][]Person)
for _, p := range people {
groups[p.City] = append(groups[p.City], p) // nil slice'ga append qilish yaxshi — u birinchi ishlatishda ajratadi
}
va oddiy xotiradagi keshlashga — odatda parallellik (concurrency) ishga kirishganda mutex bilan qo'riqlanadi (42-bob aynan shu holat uchun sync.Mutex, sync.RWMutex va sync.Map ni yoritadi):
type cache struct {
mu sync.RWMutex
data map[string]Result
}
func (c *cache) get(key string) (Result, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
Bu oxirgi misol aniq aytib qo'yish foydali bo'lgan qoidaga ishora qiladi: oddiy map'lar concurrency (bir vaqtda bir nechta goroutine orqali murojaat qilish) uchun xavfsiz emas (not thread-safe). Bir xil map'ga sinxronizatsiyasiz ikki goroutine'dan yozish shunchaki poyga (data race) xavfini tug'dirmaydi — Go runtime tizimi bir vaqtda sodir bo'layotgan map yozuvlarini darhol aniqlaydi va ma'lumotlarning buzilib ketishining oldini olish uchun dasturni qat'iy to'xtatadi (fatal error: concurrent map writes). Oddiy map atrofida sync.RWMutex qo'yish — eng xavfsiz va idiomatik yechimdir.
Oddiy map'lar multithreading / concurrency uchun xavfsiz emas. Mutex yoki sinxronizatsiyadan foydalanish shart.
Birinchisi — slice qiymatli map yozuvini qayta o'zlashtirmasdan o'zgartirish. m["numbers"] ni o'qish sizga slice sarlavhasining nusxasini beradi — asosiy massiv o'sha, lekin uzunlik va sig'im alohida — va shu nusxaga append qilish map'ga hech narsa qaytarib yozmaydi:
m := map[string][]int{"numbers": {1, 2, 3}}
s := m["numbers"]
s = append(s, 4) // bu map yozuvini emas, LOKAL s o'zgaruvchisini qayta bog'laydi
fmt.Println(m["numbers"]) // hamon [1, 2, 3]
m["numbers"] = append(m["numbers"], 4) // to'g'ri — yangi sarlavhani map'ga qaytarib yozadi
Ikkinchisi — qancha yozuv qo'shishingizni taxminan bilsangiz ham, o'lcham maslahatisiz oldindan ajratish. make(map[K]V, n) slice'lar uchun make([]T, 0, n) qanday qabul qilsa, xuddi shunday sig'im maslahatini qabul qiladi, va yakuniy o'lcham oldindan ma'lum bo'lganda uni o'tkazib yuborish map tabiiy ravishda o'sib borganda qochish mumkin bo'lgan qutilar massivini qayta ajratishga sizga xarajat keltiradi:
m := make(map[string]int, 1000) // "taxminan 1000 ta yozuv kutilmoqda" degan maslahat — to'lgan sari kamroq o'lcham o'zgartirish
Bir necha mashq, taxminan qiyinlik tartibida: satrlar slice'ini qabul qilib, map[string]int qaytaradigan so'zlar chastotasi hisoblagichini yozing. Struct'lar slice'ini ularning maydonlaridan biri bo'yicha (masalan, mahsulotlarni toifa bo'yicha) map[string][]Product ga guruhlang. map[string]int ni map[int][]string ga aylantiring (invert qiling), bir nechta kalit bir xil qiymatni ulashgan holatni qayta ishlab — bu aylantirish birga-bir moslikni birga-ko'p moslikka aylantirishi mumkinligini eslash uchun haqiqiy yaxshi mashq. Ikki map'ni birlashtiring, kalit ikkalasida ham mavjud bo'lganda nima bo'lishini aniq hal qilib. Va jiddiyroq mashq sifatida, kichik TTL keshini amalga oshiring — har bir yozuv o'zining amal qilish muddatini olib yuradigan map[string]CacheEntry, bir vaqtdagi murojaatni himoyalovchi mutex va muddati o'tgan yozuvlarni chiqarib tashlash uchun fon goroutine'i yoki dangasa (lazy) tekshiruv bilan.
Maplar arzimagan har bir Go dasturi qurilgan uchta ma'lumotlar tuzilmasi — massivlar va slice'lar, satrlar va endi map'lar — uchligini to'ldiradi. Keyingi qadamda ma'lumotdan tuzilmaga o'tamiz: bog'liq maydonlarni struct'ga qanday guruhlash va dasturlaringiz qiziqadigan haqiqiy shakllarni modellashtirishni qanday boshlash.