Goda bir qarashda juda o'xshash ko'rinadigan ikkita ketma-ketlik tipi bor — [5]int va []int bir juft qavs bilan farq qiladi — ammo ular amalda shu qadar boshqacha tutadiki, ularni chalkashtirish yangi boshlovchilar uchun jiddiy xatolarga olib kelishi mumkin. Massivlar (arrays) qat'iy o'lchamli, qiymat tipidagi (value type) va kam uchraydigan vositadir. Slice'lar (kesimlar) esa dinamik, havolaga o'xshash va har qadamda ishlatiladi. Nega bunday ekanini tushunish uchun slice aslida nima ekaniga qarash kerak: u o'z-o'zicha to'liq to'plam emas, balki ancha kattaroq bo'lishi mumkin bo'lgan tayanch massivga ochilgan oynani ifodalovchi kichik va nusxalash arzon bo'lgan sarlavhali strukturadir (slice header).
Ushbu bobdagi deyarli har bir nozik jihat — aliasing (bir xil xotiraga bir nechta havola), sig'imning (capacity) o'sishi va
appendbilan bog'liq kutilmagan holatlar — aynan shu bitta tamoyilni aniq tushunganingizdan so'ng o'z-o'zidan oydinlashadi.
Massivning o'lchami uning tipining ajralmas qismidir. [5]int va [10]int kompilyator uchun int va string kabi butunlay boshqa-boshqa tiplar hisoblanadi: [5]int parametrini kutayotgan funksiyaga [10]int massivini uzatib bo'lmaydi va ular orasida avtomatik o'tkazish yo'q.
var a [5]int // [0 0 0 0 0]
b := [5]int{1, 2, 3, 4, 5}
c := [...]int{1, 2, 3} // uzunlik elementlar sonidan avtomatik aniqlanadi: [3]int
Massivlar to'liq qiymat tiplaridir: bir massivni ikkinchisiga o'zlashtirish yoki funksiyaga argument sifatida uzatish massivning har bir elementini to'liq nusxalaydi. [3]int uchun bu arzon va xavfsiz (nusxa ko'chirilishi tufayli kutilmagan tashqi o'zgarishlar — aliasing sodir bo'lmaydi). Ammo [10000]float64 kabi ulkan massivlar uchun bu sezilmasdan unumdorlikni keskin pasaytiruvchi xatoga aylanadi: massiv funksiya chaqirilgan har safar xotirada 80 kilobaytdan to'liq nusxalanadi.
Shu sababli massivlar faqat sanoqli holatlardagina to'g'ridan-to'g'ri ishlatiladi: o'lcham tipning ma'noli qismi bo'lgan qat'iy buferlar (masalan, MD5 xeshi uchun [16]byte, IPv4 manzili uchun [4]byte), matematik matritsalar yoki slice'lar uchun orqa fondagi tayanch xotira (backing array) sifatida. Ochiq va sodda qilib aytganda, idiomatik Goda massivning eng asosiy vazifasi — slice unga tayanishi uchun xizmat qilishdir.
Slice tashqi ko'rinishidan dinamik massivga o'xshaydi, ammo ichki tuzilishi jihatidan kichik bir struct (slice header) ko'rinishida amalga oshirilgan:
type sliceHeader struct {
ptr *T // slice ko'ra oladigan birinchi elementga ko'rsatkich (pointer)
len int // slice'dagi elementlarning joriy soni (length)
cap int // qayta xotira ajratilgunga qadar sig'ishi mumkin bo'lgan elementlar soni (capacity)
}
64 bitli arxitekturada bu sarlavha bor-yo'g'i uchta mashina so'zi — ya'ni 24 baytdan iborat. Slice'da millionta element bo'lsa ham, uning sarlavhasi doimo 24 bayt bo'lib qoladi. Shuning uchun slice'ni funksiyaga uzatish yoki qaytarish millionlab ma'lumotlarni emas, faqat shu 24 baytlik sarlavhani nusxalaydi.
Aynan shu sababli slice'lar bilan ishlash juda tez: slice'ni uzatish uning ichidagi ma'lumotlarni nusxalamaydi, balki umumiy xotiraga ochilgan ixcham oynani uzatadi.
ostidagi massiv (len 10, indekslar 0-9):
[ 0][ 1][ 2][ 3][ 4][ 5][ 6][ 7][ 8][ 9]
s1 := arr[2:7] ptr -> indeks 2, len 5, cap 8 (2..9)
[ 2][ 3][ 4][ 5][ 6] <- s1 shularni ko'radi
indeks:2 6
s2 := arr[0:5] ptr -> indeks 0, len 5, cap 10 (0..9)
[ 0][ 1][ 2][ 3][ 4] <- s2 shularni ko'radi
Ushbu chizmada ikkita muhim jihatga e'tibor bering:
s1 va s2 umumiy elementlarga ega: ikkalasi ham bitta tayanch massivning 2, 3 va 4-indekslarini ko'rib turadi va ularni o'zgartira oladi. Chunki kesish (slicing) hech qachon ma'lumotdan nusxa olmaydi.cap) — slice boshlangan elementdan to tayanch massivning eng oxirigacha bo'lgan masofadir. s1 ning uzunligi 5, lekin sig'imi 8 ga teng, chunki 6-indeksdan keyin massiv tugaguncha yana 3 ta bo'sh o'rin bor. len va cap orasidagi bu bo'shliq — append funksiyasi yangi xotira ajratmasdan foydalana oladigan zaxiradir.s := make([]int, 5) // len 5, cap 5
s2 := make([]int, 5, 10) // len 5, cap 10 — 5 ta qo'shimcha zaxira o'rni bor
arr := [5]int{1, 2, 3, 4, 5}
s3 := arr[1:4] // len 3, cap 4 — [2 3 4], bitta zaxira o'rni bor
Kesish ifodasi arr[low:high] da low kiradi, high esa kirmaydi: arr[:] butun massivni oladi, arr[2:] 2-indeksdan oxirigacha, arr[:4] esa boshidan to 4-indeksgacha bo'lgan qismini oladi.
Bir nechta slice bitta umumiy tayanch massivdan foydalanishi Go tilida odatiy holat bo'lgani sababli, bitta slice orqali kiritilgan o'zgarish o'sha xotirani bo'lishayotgan boshqa slice'larda ham darhol ko'rinadi (bunga aliasing deyiladi):
original := []int{1, 2, 3, 4, 5}
s1 := original[1:4] // [2 3 4]
s2 := original[2:5] // [3 4 5]
s1[1] = 99 // original[2] ga yozadi
fmt.Println(original) // [1 2 99 4 5]
fmt.Println(s2) // [3 99 5] — s2 ham o'zgarishni ko'radi!
Bu nuqson emas, aksincha ulkan afzallik: katta hajmdagi buferlarni ortiqcha xotira sarflamasdan turli qatlamlar orasida uzatish imkonini beradi. Ammo shuni doimo yodda tutish kerakki, agar funksiya qabul qilingan slice asosida yangi qism-slice qaytarsa, u hali ham asl ma'lumotlarga bog'langan bo'ladi.
Agar sizga mustaqil va alohida nusxa kerak bo'lsa, copy funksiyasidan yoki slices.Clone dan foydalaniladi:
dst := make([]int, len(src))
copy(dst, src) // dst endi o'zining alohida mustaqil xotirasiga ega
dst2 := slices.Clone(src) // xuddi shu amal, bir qatorda (Go 1.21+)
To'liq kesish ifodasi — s[low:high:max] xotirani xavfsiz boshqarishning ajoyib usulidir: u oddiy kesish kabi ishlaydi, ammo yangi slice'ning sig'imini max - low bilan qat'iy cheklab qo'yadi. Natijada unga yangi element qo'shilganda (append), u ota-slice'ning keyingi elementlarini tasodifan ustidan yozib yubormaydi, balki majburan yangi xotira ajratadi:
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[0:3] // cap 5 — bu yerga qo'shish s1[3] ni o'zgartirib yuboradi!
s2 = append(s2, 99)
fmt.Println(s1) // [1 2 3 99 5] — kutilmagan noxushlik!
s3 := s1[0:3:3] // cap 3 — sig'im qat'iy cheklandi
s3 = append(s3, 99)
fmt.Println(s1) // [1 2 3 99 5] o'rniga [1 2 3 4 5] saqlanib qoldi — append yangi xotira ajratdi
Ikkinchi misol ustida o'tirib o'ylash foydali, chunki bu slice'lar bilan bog'liq "nega mening aloqasi yo'q ma'lumotim o'zgarib ketdi" turidagi xatolarning eng keng tarqalgan manbai: ota-slice'dan qolgan zaxira sig'imi bor qism-slice'ga qo'shish ota-slice ham ko'ra oladigan elementlarni jimgina qayta yozib yuboradi — xatosiz, ogohlantirishsiz, va quyi oqimda kimdir buzilgan ma'lumotni o'qimaguncha hech qanday belgi bermasdan.
To'liq kesish ifodasi o'sha jimgina buzilishni oddiy qayta xotira ajratishga aylantiradi.
append va slice'lar qanday o'sadiappend slice oxiriga elementlar qo'shadi, uning imzosi esa — func append(s []T, vs ...T) []T — yangi boshlovchilar tez-tez qoqiladigan qoidaga ishora: append slice qaytaradi, va siz qaytarilgan qiymatdan foydalanishingiz shart. Agar zaxira sig'im bo'lsa, append unga joyida yozadi va o'sha tayanch massivga ega, len i kattaroq slice'ni qaytaradi. Agar sig'im yetarli bo'lmasa, append butunlay yangi, kattaroq tayanch massiv ajratadi, mavjud har bir elementni nusxalaydi, yangilarini qo'shadi va yangi massivga ishora qiluvchi slice'ni qaytaradi — asl slice o'zgaruvchisi esa, agar siz unga qayta o'zlashtirmasangiz, eskisiga ishora qilib qolaveradi.
s := []int{1, 2, 3}
append(s, 4) // qaytarilgan qiymat tashlab yuborildi — s hamon [1 2 3]
s = append(s, 4) // to'g'ri — endi s [1 2 3 4]
Qayta xotira ajratish yuz berganda Go kerakli miqdorga aniq mos ravishda o'smaydi — u ortig'i bilan ajratadi, chunki sikl ichida append chaqirayotgan kod tez orada yana chaqirishi ehtimoldan yiroq emas. Runtime'ning o'sish strategiyasi kichik slice'lar uchun sig'imni taxminan ikki barobar oshiradi, keyin slice'lar kattalashgan sari kichikroq o'sish koeffitsiyentiga (taxminan 1,25 barobar atrofida) o'tadi, katta buferlarda ortiqcha xotira sarflamaslik uchun. Aniq chegaralar va koeffitsiyentlar — Go versiyalari bo'ylab o'zgargan va til spetsifikatsiyasiga kirmaydigan runtime tafsiloti. Muayyan append dan keyingi aniq sig'imga bog'liq kod yozmang, faqat sig'im har safar bittadan emas, geometrik tarzda o'sishiga tayaning — aynan shu takroriy qo'shishni har bir element uchun O(n) emas, amortizatsiyalangan O(1) qilib saqlaydi.
var s []int
for i := 0; i < 1000; i++ {
s = append(s, i) // yo'l davomida bir necha marta qayta xotira ajratadi
}
Slice oxir-oqibat qanchalik katta bo'lishini taxminan bilsangiz, uning sig'imini oldindan ajratish bu oraliq qayta ajratish va nusxalashlarning barchasidan qutqaradi:
s := make([]int, 0, 1000) // len 0, cap 1000
for i := 0; i < 1000; i++ {
s = append(s, i) // hech qachon qayta xotira ajratmaydi
}
Bir slice'ning elementlarini boshqasiga qo'shish uchun ... yoyish operatoridan foydalaniladi:
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
s1 = append(s1, s2...)
nil slice'lar, bo'sh slice'lar va clearnil slice bilan nol uzunlikdagi slice nozik farq qiladigan qiymatlar, ammo ahamiyatli deyarli har bir amal uchun bir xil tutadi:
var nilSlice []int // nil, len 0, cap 0
emptySlice := []int{} // nil emas, len 0, cap 0
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
fmt.Println(len(nilSlice), len(emptySlice)) // 0 0
nilSlice = append(nilSlice, 1) // to'g'ri — append nil slice'larni uddalaydi
emptySlice = append(emptySlice, 1) // bu ham to'g'ri
append nil slice'da to'g'ri ishlagani uchun (nusxalash uchun shunchaki hech narsa yo'q, birinchi qo'shish esa xotira ajratadi), var s []T nol qiymati append bilan to'ldirib boradigan slice'ni e'lon qilishning mutlaqo idiomatik usuli, va u shu maqsad uchun []T{} dan biroz afzalroq, chunki darhol tashlab yuboriladigan bo'sh tayanch massivni ajratmaydi. Deyarli har doim sizga kerak bo'ladigan tekshiruv — len(s) == 0, u ikkala holat uchun ham rost. Ochiq s == nil tekshiruvini "bu umuman initsializatsiya qilinganmi" degan savol "u hozir bo'shmi" degan savoldan ma'noli farq qiladigan kamroq uchraydigan holat uchun saqlab qo'ying — masalan, JSON'da tushirib qoldirilgan maydonni (nil) ochiq bo'sh massiv sifatida yuborilganidan ([]T{}) ajratish uchun; encoding/json bu farqni saqlaydi.
Mavjud slice'ni tayanch massivini (demak, sig'imini) qayta ishlatish uchun saqlab qolgan holda bo'shatish kerak bo'lsa, Go 1.21 da qo'shilgan clear ichki funksiyasi ishlatiladi. Diqqat: slice'dagi clear(s) har bir elementni nollaydi, ammo len(s) ni o'zgarishsiz qoldiradi — u uzunlikni emas, mazmunni tozalaydi. Slice'ning sig'imini qayta ishlatib, uzunligini nolga qaytarish uchun idioma hamon s = s[:0]:
s = s[:0] // len 0, cap o'zgarmagan — xuddi shu massivga qo'shishga tayyor
Elementni indeks bo'yicha o'chirish, uning atrofini append bilan ulab:
func removeAt(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
Bu idiomatik, ammo aniq tushunish foydali: u i dan keyingi har bir elementni asl tayanch massivda, joyida bitta chapga suradi — u asl slice'ning yangi (qisqaroq) uzunligidan keyingi mazmunini saqlamaydi, shuning uchun uni boshqa joyda o'zgartirilmagan nusxasi kerak bo'lgan slice'da chaqirmang. Tartib ahamiyatli bo'lmasa, maqsadli elementni oxirgisi bilan almashtirib, so'ng qisqartirish arzonroq O(1) muqobil:
func removeAtUnordered(s []int, i int) []int {
s[i] = s[len(s)-1]
return s[:len(s)-1]
}
slices paketi bu amallarning ko'pchiligini to'g'ridan-to'g'ri qoplaydi, aliasing (xotiradagi o'zgarishlar) xususiyatlari esa har bir funksiya uchun aniq hujjatlashtirilgan: slices.Delete, slices.Insert, slices.Contains, slices.Index, slices.Reverse, slices.Equal, hamda Go 1.22 dan boshlab slices.Concat. Ular qoplaydigan har qanday narsa uchun ekvivalent siklni qo'lda yozish o'rniga shularga murojaat qilish endi idiomatikroq tanlov; siklni esa mantiq ularning biriga toza mos kelmaganda yozing, odat bo'yicha emas.
evens := slices.DeleteFunc(slices.Clone(numbers), func(n int) bool {
return n%2 != 0
})
slices paketi Go 1.23 da oldingi bobda ko'rib chiqilgan funksiya ustidan range iteratorlariga qurilgan ikkinchi funksiyalar oilasini oldi: slices.All(s) (indeks, qiymat) juftliklari ustidan iterator qaytaradi, slices.Values(s) faqat qiymatlarni beradi, slices.Backward(s) esa slice bo'ylab teskari yo'nalishda yuradi — hammasini oraliq slice'ni hosil qilmasdan to'g'ridan-to'g'ri for range da ishlatish mumkin:
for i, v := range slices.All(numbers) {
fmt.Println(i, v) // `for i, v := range numbers` bilan bir xil shakl
}
for _, v := range slices.Backward(numbers) {
fmt.Println(v) // oxirgi element birinchi bo'lib
}
slices.Backward ayniqsa birga adashish (off-by-one) xatosini qilish oson bo'lgan qo'lda yozilgan for i := len(s) - 1; i >= 0; i-- siklining o'rnini bosadi; iterator variantini noto'g'ri indekslab bo'lmaydi, chunki u yerda yoziladigan indeks arifmetikasining o'zi yo'q.
Teskari yo'nalishda — iteratorni qaytadan aniq slice'ga aylantirish — slices.Collect(seq) iter.Seq[V] ning qiymatlarini yangi []V ga to'playdi, slices.AppendSeq(s, seq) esa yangi slice ajratish o'rniga ularni mavjud slice'ga qo'shadi. slices.Sorted(seq), slices.SortedFunc(seq, cmp) va slices.SortedStableFunc(seq, cmp) to'plash va saralashni bitta qadamda birlashtiradi — bu faqat slices ning o'zidan emas, iterator ishlab chiqaradigan har qanday funksiya bilan foydali (map'lar haqidagi bob buni maps.Keys bilan birga keng tarqalgan idiomada ko'rsatadi: map'ning saralangan kalitlar ro'yxatini bitta ifodada olish uchun slices.Sorted(maps.Keys(m))).
Paketni yana ikkita qo'shimcha to'ldiradi: slices.Chunk(s, n) s ning ketma-ket n elementli qism-slice'lari ustidan iterator qaytaradi — katta slice'ni qat'iy o'lchamdagi to'plamlarda, barcha to'plam chegaralarini oldindan hisoblamasdan qayta ishlash uchun foydali; slices.Repeat(s, count) esa s ni count marta takrorlagan yangi slice qaytaradi. Go 1.27 yana slices.CutLast(s, sep) ni qo'shdi — u slice'ni ajratuvchi elementning oxirgi uchrashi atrofida kesadi, bu paketdagi mavjud "birinchi uchrash atrofida kesish" xatti-harakatining hamkori bo'lib, masalan, yo'lni oxirgi ajratuvchisi bo'yicha butun slice bo'ylab qo'lda yurmasdan bo'lish uchun qulay.
Amalda "samarali slice kodi yozish" degani nimani anglatishini uchta odat qoplaydi. Oxirgi o'lcham ma'lum yoki taxmin qilinadigan bo'lsa, takroriy qayta xotira ajratishdan qochish uchun sig'imni make([]T, 0, n) bilan oldindan ajrating. Bu ahamiyatli bo'lgan qizg'in yo'llarda har safar yangi slice ajratish o'rniga slice'ning tayanch massivini s[:0] bilan qisqartirib, chaqiruvlar bo'ylab qayta ishlating. Va o'zingizga tegishli bo'lmagan kattaroq buferga kesim qilayotganda to'liq kesish ifodasini (s[low:high:high]) afzal ko'ring — u sig'imni cheklab, bir necha chaqiruv kadridan keyingi beg'ubor ko'rinadigan append asl egasi hamon tayanadigan xotirani buzishiga yo'l qo'ymaydi.
Massivlar va slice'lar — bu Goning dunyoga bayt nuqtai nazaridan qarashi o'zining keyingi nozik jihati bilan uchrashadigan joy ham: Go satri []byte ga juda yaqin qarindosh, byte slice'lari uchun ahamiyatli bo'lgan bayt/belgi farqi esa Unicode ishtirok etgach yanada muhimroq bo'ladi. 16-bob aynan shu yerdan davom etadi: Go matnni ichkarida qanday ifodalaydi, baytlarni sanash bilan belgilarni sanash orasidagi farq nimada, va satrlar bilan ham to'g'ri, ham samarali ishlash qanday bo'ladi.