Goda struct'ni boshqa o'zgaruvchiga o'zlashtirish har doim uning to'liq nusxasini yaratadi. Slice yoki map'ni o'zlashtirish esa tashqaridan xuddi shunday ko'rinsa-da (= operatori), amalda ancha kichik narsani nusxalaydi va asosiy ma'lumotlar xotirasini birgalikda ulashadi (aliasing). Ushbu farqni chuqur anglash Go dasturlashidagi xatolarning butun bir turkumini oldini olishga yordam beradi. Bu tasodifiy emas: slice va map'lar mohiyatan ulashilgan xotiraga ishora qiluvchi ixcham sarlavhalardir (headers). Sarlavhada aynan nima saqlanishini tushunish esa kodingiz qachon mustaqil nusxa yaratishi, qachon esa umumiy xotiraga murojaat qilishini aniq bilish imkonini beradi.
Slice va map'lar — ulashilgan xotiraga ishora qiluvchi kichik sarlavhalardir, shuning uchun ularni o'zlashtirish ma'lumotning o'zini emas, faqat sarlavhasini nusxalaydi.
Slice sarlavhasi uchta qismdan iborat: asosiy massivga pointer, uzunlik (len) va sig'im (cap). Mavjud massiv yoki slice'dan qism (sub-slice) kesib olinganda, xuddi shu asosiy massivga ishora qiluvchi yangi sarlavha hosil bo'ladi. Shu sababli ikkita slice bitta massiv xotirasini bir vaqtda ko'rib turishi mumkin:
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4] // [2, 3, 4] — arr ga qarash oynasi
s2 := arr[2:5] // [3, 4, 5] — arr ga qarash oynasi, s1 bilan ustma-ust tushadi
s1[0] = 99
fmt.Println(s2[0]) // 99 — s1[0] va s2[0] bir xil massiv elementi
Bir slice o'zgaruvchisini boshqasiga o'zlashtirish asosiy massiv elementlarini emas, uch qismli sarlavhani nusxalaydi — shuning uchun yangi o'zgaruvchi hamon asl ma'lumotlarga ko'rsatib turadi:
original := []int{1, 2, 3}
alias := original // faqat sarlavhani nusxalaydi: pointer, len, cap — elementlarni emas
alias[0] = 99
fmt.Println(original[0]) // 99 — bir xil asosiy massiv
Slice'larni funksiyalarga uzatishni juda arzon qiladigan narsa ham aynan shu (uchta mashina so'zini nusxalash slice qanchalik katta bo'lishidan qat'i nazar deyarli bepul). Ammo ehtiyot bo'lmasangiz, funksiya ichida slice elementlariga yozish chaqiruvchining asl ma'lumotlarini o'zgartirib yuborishi mumkin.
Maplar ham xuddi shunday havola (reference) semantikasiga ega. Ularning ichki ko'rinishi xesh-jadval deskriptoriga bo'lgan pointer'dan iborat. Shuning uchun map o'zgaruvchisini nusxalash jadval tarkibini emas, o'sha jadvalga bo'lgan pointer'ni nusxalaydi:
m1 := map[string]int{"a": 1, "b": 2}
m2 := m1 // m2 XUDDI SHU asosiy jadvalga ishora qiladi
m2["c"] = 3
fmt.Println(m1["c"]) // 3 — ikkalasi ham bitta jadval ustida ishlaydi
Slice'lardan farqli o'laroq, map'larda bo'laklab olish (sub-slice) imkoni yo'q — bitta map'ni ikkinchisiga o'zlashtirish har doim to'liq umumiy havolani (aliasing) vujudga keltiradi.
Slice va map o'zlashtirish faqat sarlavhani nusxalagani uchun, mustaqil nusxa aniq, element-element qurilishi kerak. Slice'lar uchun o'rnatilgan copy funksiyasi yoki nil slice'ga append ikkalasi ham ishlaydi:
original := []int{1, 2, 3, 4, 5}
dup := make([]int, len(original))
copy(dup, original) // len(dup) ta elementni dup ga nusxalaydi
dup2 := append([]int(nil), original...) // xuddi shunday keng tarqalgan idioma
(E'tibor bering, nusxaning manzili copy emas, dup deb nomlangan — o'rnatilgan copy funksiyasini xuddi shu nomli lokal o'zgaruvchi bilan shadow qilish haqiqiy va hayratlanarli darajada oson xato, va shadow qilingandan keyin copy o'sha scope'ning qolgan qismida o'rnatilgan funksiyaga murojaat qilishni to'xtatadi, bu esa uning keyingi har qanday chaqiruvini shadow qilingan nomni funksiya sifatida ishlatishga urinmaguningizcha kompilyator xato deb belgilamaydigan tarzda buzadi.)
Maplar uchun o'rnatilgan ekvivalent yo'q — siz sikl yozasiz:
original := map[string]int{"a": 1, "b": 2}
dup := make(map[string]int, len(original))
for k, v := range original {
dup[k] = v
}
Go 1.21 standart maps paketiga aynan shuni bajaradigan maps.Cloneni qo'shdi va siklni qo'lda yozish o'rniga unga murojaat qilish foydali:
dup := maps.Clone(original)
copy ham, maps.Clone ham sayoz (shallow) — agar slice yoki map'ning element tipi o'zi slice, map yoki pointer bo'lsa, nusxa o'sha ichma-ich reference'ni avvalgidek ulashadi. Slice va map'ni o'z ichiga olgan struct nuqtani bevosita ko'rsatadi: struct qiymatini nusxalash struct'ning o'z maydonlarini nusxalaydi, lekin uning ichidagi slice va map maydonlari hamon asl bilan bir xil asosiy xotiraga ishora qiladi:
type Data struct {
Values []int
Counts map[string]int
}
d1 := Data{Values: []int{1, 2, 3}, Counts: map[string]int{"a": 1}}
d2 := d1 // struct'ning uch so'zlik sarlavhalarini nusxalaydi, ular ishora qilgan ma'lumotni emas
d2.Values[0] = 99
fmt.Println(d1.Values[0]) // 99 — hamon bir xil asosiy massiv
d2.Counts["b"] = 2
fmt.Println(d1.Counts["b"]) // 2 — hamon bir xil map
Haqiqiy chuqur nusxa har bir reference-tipli maydonga aniq rekursiv tushishi kerak:
func (d Data) DeepCopy() Data {
valuesCopy := make([]int, len(d.Values))
copy(valuesCopy, d.Values)
countsCopy := make(map[string]int, len(d.Counts))
for k, v := range d.Counts {
countsCopy[k] = v
}
return Data{Values: valuesCopy, Counts: countsCopy}
}
Til yoki standart kutubxonada avtomatik chuqur nusxalash imkoniyati yo'q — bir nechta ichma-ich reference maydonli struct uchun buni qo'lda yozish (yoki generatsiya qilish) haqiqiy mustaqillikni xohlashning shunchaki narxi.
Yuqoridagi ulashish xatti-harakati shunchaki o'zlashtirish haqidagi fakt emas — u slice va map receiver yoki qaytaruvchi funksiyalar va metodlarni qanday loyihalashingizni bevosita shakllantiradi, chunki chaqiruvchida ularga berilgan ma'lumot ulashilganmi yoki mustaqilmi degan sintaktik ishora yo'q.
Ichki slice yoki map'ni to'g'ridan-to'g'ri qaytarish chaqiruvchiga tipingizning ichki holatiga jonli reference beradi, uni siz bilmagan holda o'zgartirishi mumkin:
type Buffer struct {
data []byte
}
func (b *Buffer) Data() []byte {
return b.data // chaqiruvchi endi b ning ichki qismlariga to'g'ridan-to'g'ri yozish huquqiga ega
}
buf := &Buffer{data: []byte{1, 2, 3}}
d := buf.Data()
d[0] = 99
fmt.Println(buf.data[0]) // 99 — chaqiruvchi ichki holatni jimgina o'zgartirdi
Agar bu ko'zda tutilgan shartnoma bo'lmasa, o'rniga nusxa qaytaring:
func (b *Buffer) Data() []byte {
result := make([]byte, len(b.data))
copy(result, b.data)
return result
}
Xuddi shu mulohaza teskari yo'nalishda slice yoki map parametrini qabul qilishga ham tegishli: slice argumentiga yozadigan funksiya chaqiruvchining haqiqiy ma'lumotini o'zgartirmoqda, bu ba'zan aynan maqsad (masalan, joyida saralash), ba'zan esa chaqiruvchi rozi bo'lmagan kutilmagan yon ta'sir:
func process(data []int) {
data[0] = 99 // chaqiruvchining slice'ini jimgina o'zgartiradi
}
Kompilyator "ataylab joyida o'zgartirish"ni "tasodifiy yon ta'sir"dan ajrata olmaydi — ikkalasi bir xil kompilyatsiya bo'ladi — shuning uchun mas'uliyat butunlay hujjatlashtirish va nomlashga tushadi. Keng tarqalgan kelishuv — joyida o'zgartirishni funksiya nomi yoki doc comment'idan aniq qilish (sortInPlace yoki shartnomani aniq bayon qiluvchi izoh) va aniq mutator bo'lishi ko'zda tutilmagan hamma narsa uchun o'zgartirmaslikni standart qilish — avval nusxa ol, so'ng nusxani o'zgartir, so'ng uni qaytar.
Buning bitta aniq versiyasi deyarli hammani bir marta, odatda erta, hayratlantiradi: slice'ni funksiyaga uzatish va shu funksiya ichidagi append chaqiruvchining slice'ini o'stirishini kutish.
func appendValue(s []int, v int) {
s = append(s, v) // LOKAL s parametrini qayta o'zlashtiradi, chaqiruvchining o'zgaruvchisini emas
}
numbers := []int{1, 2, 3}
appendValue(numbers, 4)
fmt.Println(numbers) // [1, 2, 3] — butunlay o'zgarmagan
append, mavjud asosiy massivda bo'sh sig'im bor-yo'qligiga qarab, yangi asosiy massiv ajratishi yoki ajratmasligi mumkin, va har qanday holatda natija lokal s parametriga o'zlashtiriladigan yangi slice sarlavhasi bo'ladi — bu funksiya qaytgandan keyin chaqiruvchining o'zgaruvchisiga hech qanday ta'sir qilmaydi, chunki slice parametrlari, Godagi hamma narsa kabi, qiymat bo'yicha uzatiladi. Tuzatish — (ehtimol yangi) slice'ni qaytarish va chaqiruvchiga uni qayta o'zlashtirishga imkon berish — appendning o'zi har bir chaqiruv joyida talab qiladigan xuddi shu andoza:
func appendValue(s []int, v int) []int {
return append(s, v)
}
numbers = appendValue(numbers, 4) // endi numbers yangilandi
Muqobil — slice'ga pointer, *[]int — ham ishlaydi va ba'zan slice maydoniga ega bo'lgan va uni o'stirish uchun metod xohlaydigan tip ichida to'g'ri vosita, lekin mustaqil funksiya uchun yangi slice'ni qaytarish oddiyroq, idiomatikroq va appendning o'zi ishlatilishi mo'ljallangan tarzni aks ettiradi.
Bir necha yaxshi odat ma'lumotlarni ulashishdagi noxush xatolarning oldini oladi. Agar funksiya nusxa olish o'rniga ichki holatga to'g'ridan-to'g'ri havola bersa, buni izohlarda aniq ko'rsating (masalan, "qaytarilgan slays ichki massivga havola qiladi; uni o'zgartirmang"). Ommaviy API yaratishda ikkilanib qolsangiz, accessor'lardan nusxa qaytarishni standart qilib oling; xotiraning kichik sarfi ichki holat tasodifan buzilishidan kelib chiqadigan xatolarni qidirishdan ancha arzondir. Agar funksiya parametr sifatida uzatilgan slays yoki map'ni aynan joyida (in-place) o'zgartirishi kerak bo'lsa, buni funksiya nomida (SortInPlace) aks ettiring. Kichik ma'lumotlar uchun esa o'zgarmas (immutable) qiymat tiplarini loyihalash maqsadga muvofiqdir — ya'ni mavjud holatni o'zgartirish o'rniga yangi o'zgartirilgan qiymat qaytaruvchi value receiver metodlaridan foydalaning:
type Point struct{ X, Y float64 }
func (p Point) Translate(dx, dy float64) Point {
return Point{X: p.X + dx, Y: p.Y + dy} // p ning o'zi hech qachon o'zgartirilmaydi
}
p1 := Point{X: 1, Y: 2}
p2 := p1.Translate(3, 4) // p1 o'zgarmagan; p2 alohida qiymat
Bularning barchasi tilning maxsus cheklovlariga emas, toza arxitektura va intizomga tayanadi — kodingizdan foydalanuvchilar uning ichki tuzilishini o'qimasdan ham xavfsiz va ishonchli ishlatishlari kerak.
[]intning har bir elementini ikki barobar oshiradigan va asl slaysga tegmasdan yangi slice qaytaradigan funksiya yozing (nusxa va ulashish farqini amalda ko'rish uchun).Increment metodi mavjud holatni o'zgartirish o'rniga yangi qiymatli Counter qaytaradigan o'zgarmas (immutable) Counter yarating va uni pointer receiver'li o'zgaruvchan versiya bilan solishtiring.map[string]intni SafeMap struct'iga o'rang, uning Get, Set va Keys metodlari hech qachon ichki map'ga tashqi havola (aliasing) bermasin — xususan Keys yangi []string qaytarsin.ShallowCopy) va to'liq chuqur nusxa (DeepCopy) metodlarini yozing, hamda ularning farqini ko'rsatuvchi unit test yozing.Ulashish va nusxalash keyingi bob mavzusi — Garbage Collection bilan bevosita bog'liq: GC dasturingiz qancha obyekt yaratishi va ularga havolalar qancha vaqt yashashi bilan ishlaydi. Keraksiz nusxalash va uzoq vaqt saqlanib qolgan havolalar aynan GC unumdorligiga ta'sir qiluvchi omillardir.