Pointer — bu qiymatning xotiradagi manzilidan boshqa narsa emas. Butun tushuncha shundan iborat — undan tashqari hech qanday sehr yo'q — lekin qiymat o'rniga manzilni ushlab turishning oqibatlari Goda qabul qiladigan deyarli har bir dizayn qaroringiz bo'ylab tarqaladi: metod o'z receiver'ini o'zgartira oladimi, struct'ni funksiyaga uzatish qimmatmi, dasturingizning ikki qismi bir xil ma'lumotga qarayaptimi yoki mustaqil nusxalargami. Pointer'lar bilan qulay bo'lish doimiy paydo bo'ladigan oddiy savol bilan qulay bo'lishni anglatadi: menga nusxa kerakmi, yoki ulashish?
Pointer — bu qiymatning xotiradagi manzili.
Go pointer'lar bilan ishlash uchun aniq ikkita operator beradi, va ular bir-birining teskarisi. & qiymatning manzilini oladi va unga pointer hosil qiladi. * pointer'ni dereferens qiladi va u ko'rsatayotgan qiymatni beradi.
x := 42
p := &x // p — *int, x ning manzilini ushlab turadi
fmt.Println(*p) // 42 — shu manzilda saqlangan qiymat
*p = 100 // pointer orqali yozish
fmt.Println(x) // 100 — x ning o'zi o'zgardi
*int tipi "int'ga pointer" deb o'qiladi, va bu qoida barcha tiplarga tegishli: *Person — Person struct'iga pointer, *[]byte — slice'ga pointer va hokazo. Boshlang'ich manzil biriktirilmagan pointer o'zgaruvchisining nol qiymati nil bo'ladi. Nil pointer'ni dereferens qilish dastur ishlayotgan vaqtda (runtime) mashhur panic: runtime error: invalid memory address or nil pointer dereference xatosini keltirib chiqaradi. Agar pointer nil bo'lishi ehtimoli bo'lsa, uni dereferens qilishdan oldin p != nil sharti bilan tekshirish yoki API'ni nil pointer kelmaydigan qilib loyihalash zarur.
C/C++ dan farqli o'laroq, Goda pointer arifmetikasi yo'q. Pointer'ni oshirish (p++), unga son qo'shish (p + 1) yoki xotirani baytma-bayt qo'lda kezish mumkin emas — bular kompilyatsiya xatosidir. Bu ataylab qilingan xavfsizlik cheklovidir: pointer arifmetikasi xotirani buzish va xavfsizlik zaifliklarining asosiy manbalaridan biri bo'lgani sababli, Go uni xavfsiz kodda butunlay taqiqlagan (o'ta zarur past darajadagi amallar uchun unsafe paketi mavjud bo'lib, u alohida bobda ko'rib chiqiladi).
new o'rnatilgan funksiyasiMavjud o'zgaruvchining manzilini & yordamida olish eng keng tarqalgan usuldir. Ammo ba'zan alohida nomlangan o'zgaruvchi yaratmasdan, nol qiymatli yangi xotira sohasiga pointer kerak bo'ladi. new(T) funksiyasi aynan shu vazifani bajaradi: u T tipi uchun xotira ajratadi, uni nol qiymatlar bilan to'ldiradi va unga ko'rsatuvchi *T manzilini qaytaradi.
p := new(int) // p — *int, nol qilingan int'ga ko'rsatadi
fmt.Println(*p) // 0
q := new(Person) // q — *Person, barcha maydonlar nol qiymatli
Struct'lar uchun new(T) va &T{} amalda bir xil natija beradi: new(Person) ham, &Person{} ham barcha maydonlari nol qiymat bilan to'ldirilgan *Person pointer'ini qaytaradi. Shu sababli Go dasturchilari ko'pincha to'g'ridan-to'g'ri kompozit literaldan (&Person{}) foydalanishni afzal ko'rishadi, chunki unda maydonlarni boshlang'ich qiymatlar bilan darhol to'ldirish mumkin.
Go 1.26 dan boshlab new bu bo'shliqni yopadi: u endi shunchaki tip nomi emas, qiymatning boshlang'ich mazmunini beruvchi ifodani ham qabul qiladi.
// Go 1.26 dan oldin — nomlangan oraliq o'zgaruvchi kerak
years := yearsSince(born)
p := &years
// Go 1.26 va undan keyin — ajratish va initsializatsiyani bir qadamda bajarish
p := new(yearsSince(born))
Bu new asosan nima qilishini o'zgartirmaydi — u hamon "ajrat, so'ng menga manzilini ber" — u shunchaki manzilini olish uchungina vaqtinchalik lokal o'zgaruvchi kerakligini olib tashlaydi. Bu kichik o'zgarish, lekin har bir maydon hisoblangan ifodadan o'rnatiladigan option struct'lari yoki konfiguratsiya qiymatlarini quradigan kodda paydo bo'ladigan narsa.
Pointer'ga murojaat qilishning uchta alohida sababi bor, va ularni ajratish foydali, chunki ular turli xil mulohazani talab qiladi.
Funksiya chaqiruvchining qiymatini o'zgartirishi kerak. Go hamma narsani qiymat bo'yicha uzatadi — argumentlar, jumladan struct argumentlari, chaqirilgan funksiyaga nusxalanadi. Oddiy int qabul qiladigan funksiya chaqiruvchining o'zgaruvchisini hech qachon o'zgartira olmaydi, chunki u faqat nusxa bilan ishlaydi:
func increment(x int) {
x++ // faqat lokal nusxani o'zgartiradi
}
value := 5
increment(value)
fmt.Println(value) // hamon 5
Buning o'rniga pointer uzatish funksiyaga chaqiruvchi o'zgaruvchisining manzilini beradi, shuning uchun shu pointer orqali yozuvlar chaqiruv qaytgandan keyin ko'rinadi:
func increment(x *int) {
*x++
}
value := 5
increment(&value)
fmt.Println(value) // 6
Holatni o'zgartiradigan metodlar uchun pointer receiver'lar aynan shu sababli mavjud:
type Counter struct{ value int }
func (c *Counter) Increment() { c.value++ }
Bu yerda value receiver Counterning nusxasini oshirar va metod qaytishi bilan uni tashlab yuborardi — kompilyator ushlamaydigan boshlovchi xatosi, chunki u sintaktik jihatdan yaroqli, shunchaki foydasiz.
Katta qiymatni nusxalash narxidan qochish kerak. Struct har safar qiymat bo'yicha uzatilganda — argument, qaytariladigan qiymat yoki interface ichiga — uning to'liq mazmuni nusxalanadi. Point{X, Y float64} uchun bu bepul. O'nlab maydonli yoki katta massivni o'z ichiga olgan struct uchun nusxa har chaqiruvda takrorlanadigan haqiqiy ish. *LargeStruct uzatish, ishora qilingan struct qanchalik katta bo'lishidan qat'i nazar, faqat pointer'ni (bitta mashina so'zi) nusxalaydi.
Bu qachon o'zini oqlay boshlashini belgilaydigan universal o'lcham chegarasi yo'q — u chaqiruv yo'li qanchalik qizg'inligiga bog'liq — lekin qoida sifatida, bir necha mashina so'zidan katta struct'lar tez-tez nusxalansa, pointer bilan uzatish foydali, va standart kutubxona aynan shu sababli shu tomonga moyil (*bytes.Buffer, *http.Request, *sql.DB — hammasi mazmunli ichki holatli struct'larga pointer'lar).
Dasturning turli qismlari bitta umumiy ma'lumot bilan ishlashi (holatni ulashish) kerak bo'lganda. Ikki goroutine, ikkita ma'lumot tuzilmasi yoki funksiya chaqiruvlari o'rtasida umumiy pointer ishlatilsa, bir tomon qilgan o'zgarishlar darhol ikkinchi tomonga ko'rinadi. Bu keshlar yoki bog'langan tuzilmalar (linked list, tree) uchun juda qo'l kelsa-da, parallel ishlashda ma'lumotlar poygasi (data race) xavfini keltirib chiqarishi mumkin.
Pointer qachon kerakligini bilish qanchalik muhim bo'lsa, uni qachon ishlatmaslik kerakligini tushunish ham shunchalik zarur. Kichik qiymatlar — int, bool, float64 va shunga o'xshash sodda turlarni nusxalash juda arzon. Ularni pointer'ga o'rash arzon nusxalash o'rniga heap'da xotira ajratishga va har murojaatda qo'shimcha dereferens sarfiga olib keladi:
// keraksiz: int'ga pointer int'ning o'zidan qimmatroqqa tushadi
func add(a, b *int) int { return *a + *b }
// idiomatik va to'g'ri
func add(a, b int) int { return a + b }
Slice, map va channel bilan ishlashda keng tarqalgan xato mavjud: ularning o'zi ichki tuzilishi bo'yicha havolaga (reference) asoslangan. Slice — massiv manziliga ko'rsatuvchi kichik sarlavha (pointer, len, cap); map sarlavhasi — xesh-jadval ko'rsatkichi; channel esa runtime'dagi kanal tuzilmasiga pointer'dir. Ularni funksiyaga uzatganda faqatgina kichik sarlavha nusxalanadi, asosiy massiv yoki jadval emas. Shu sababli funksiyaga slice uzatilganda qo'shimcha pointer sintaksisisiz uning elementlarini o'zgartirish mumkin:
// keraksiz murakkablik
func process(s *[]int) { *s = append(*s, 1) }
// idiomatik yondashuv
func process(s []int) []int { return append(s, 1) }
*[]int (slice'ga pointer) faqat bitta holatda kerak bo'lishi mumkin: agar funksiya ichida slice sarlavhasining o'zini (uning xotiradagi massiv ko'rsatkichi, uzunligi yoki sig'imini) chaqiruvchi o'zgaruvchisida butunlay yangilash talab etilsa. Biroq bu amaliyotda juda kamdan-kam uchraydi — funksiyadan yangi slaysni qaytarish va chaqiruvchida uni o'zlashtirish ancha toza va tushunarli usuldir.
Interfeyslar ham shunday. Interface qiymatining o'zi allaqachon ikki so'zli juftlikdir (tip deskriptori va ma'lumot ko'rsatkichi). Shu bois *io.Writer kabi "interfeysga pointer" yozish deyarli har doim xato dizayndan dalolat beradi. Har doim *io.Writer emas, shunchaki io.Writer yozing.
Pointer nil bo'lishi mumkinligi sababli, pointer'lar Goning ixtiyoriy yoki nullable tipga eng yaqin ekvivalenti vazifasini ham bajaradi. *string tipidagi struct maydoni "bo'sh satr"ni "umuman qiymat yo'q"dan oddiy string qila olmaydigan tarzda ajrata oladi:
type Person struct {
Name string
Email *string // nil "faylda elektron pochta yo'q" degani
}
Bu andoza tashqi ma'lumotlarni aks ettiradigan kodda keng tarqalgan — nullable ustunli ma'lumotlar bazasi qatorlari, ixtiyoriy maydonli JSON payload'lari — bunda asosiy tipning nol qiymati qonuniy, mazmunli qiymat bo'lishi mumkin va "yo'q" deyishning alohida usuli kerak. U narx bilan keladi: Emailning har bir o'qilishi nil holatini hisobga olishi kerak, bu aynan errors.New("email is nil") uslubidagi himoyalovchi kod unutishning oldini olish uchun mavjud bo'lgan tekshiruv turi. Nol qiymat allaqachon aniq bo'lgan joyda (bo'sh slice, nol sanog'i), oddiy qiymat tipi soddaroq va aslida kerak bo'lmagan "ixtiyoriylik"ni olish uchun pointer'ga murojaat qilishga hech qanday sabab yo'q.
C'dan kelgan yangi Go dasturchilari ba'zan shunday koddan cho'chiydi:
func newPerson(name string) *Person {
p := &Person{Name: name}
return p
}
C'da lokal o'zgaruvchining manzilini qaytarish aniqlanmagan xatti-harakat — stack frame'i funksiya qaytgan zahoti yo'qoladi va pointer tez orada qayta ishlatiladigan xotiraga ishora qiladi. Goda bu shunchaki xavfsiz emas, idiomatik ham, oldingi bobda ko'rilgan sabab bilan: escape analysis p qaytarilayotganini payqaydi va uni avvalo stack o'rniga heap'da ajratadi. Pointer unga reference ushlab turgan har qanday narsa mavjud ekan yaroqli, bas — bo'shatilgan stack frame'iga osilib qolgan pointer'ning ekvivalenti yo'q, chunki xotiraga hamon erishish mumkin ekan, Goning GC'si uni qaytarib olmaydi.
Demak, pointer'lar — kichik, o'tkir vosita: chaqiruvchining qiymatini o'zgartirish, sezilarli narsani nusxalashdan qochish yoki holatni ataylab ulashish kerak bo'lganda unga murojaat qiling, qolgan hamma joyda oddiy qiymatlarni tegmasdan qoldiring.
swap(a, b *int) va ikkita lokal o'zgaruvchida ishlaganini isbotlaydigan kichik main yozing.nil qaytaradigan findMax(numbers []int) *int yozing. Bu yerda pointer qaytarish — qiymat plyus bool o'rniga — yaxshiroq tanlovmi yoki emasmi, o'ylab ko'ring.ptr nil bo'lmaganda *ptr, aks holda fallback qaytaradigan safeDeref(ptr *int, fallback int) int yozing va uni atrofdagi kichik kod bo'lagidan nil tekshiruvini yo'qotish uchun ishlating.setValue(pp **int, value int) yozing. Pointer'ga pointer'lar ilova kodida kamdan-kam uchraydi, lekin chaqiruvchining pointer o'zgaruvchisini qayta o'zlashtirishi kerak bo'lgan kodda keng tarqalgan (ba'zi C interop va CLI flag tahlili kodi shunday ishlaydi) — uni yozganingizdan keyin, yangi *intni shunchaki qaytarish o'rniga **intga chindan murojaat qiladigan bitta haqiqiy vaziyatni o'ylab toping.Keyingi bob, "O'zgaruvchanlik va ma'lumotni ulashish", yuqoridagi uchinchi holat qoldirgan joydan aynan davom etadi — slice'lar va map'lar aniq pointer ko'rinmasa ham allaqachon ulashilgan reference'lar kabi tutadi, va bu funksiyaning yoki tipning ochiq yuzasini qanday loyihalashingiz uchun haqiqiy oqibatlarga ega.