Tarkibga o'tish

Ko‘rsatkichlar (pointers)

Ko‘rsatkich (pointer) boshqa qiymatning xotira manzilini saqlaydi. U bir qiymatni turli joydan o‘zgartirish, katta tuzilmani nusxalamasdan uzatish yoki qiymat mavjud emasligini nil bilan ifodalash kerak bo‘lganda ishlatiladi.

& va * operatorlari

  • &xx o‘zgaruvchisining manzilini oladi.
  • *pp ko‘rsatgan manzildagi qiymatni o‘qiydi yoki o‘zgartiradi.
  • *intint qiymatiga ko‘rsatkich turini bildiradi.
package main

import "fmt"

func main() {
    x := 10
    p := &x

    fmt.Println("Qiymat:", x)
    fmt.Println("Ko‘rsatkich orqali:", *p)
    *p = 20
    fmt.Println("Yangi qiymat:", x)
}

Natija:

Qiymat: 10
Ko‘rsatkich orqali: 10
Yangi qiymat: 20

pning turi *int. *p = 20 yangi ko‘rsatkich yaratmaydi; p ko‘rsatib turgan x qiymatini o‘zgartiradi. Manzilni %p bilan chiqarish mumkin, ammo uning aniq qiymati har ishga tushirishda o‘zgarishi mumkin va dastur mantig‘i unga tayanmasligi kerak.

Qiymat va ko‘rsatkichni funksiyaga uzatish

Go’da argumentlar funksiyaga qiymat bo‘yicha uzatiladi. Oddiy qiymat uzatilsa, funksiya uning nusxasini oladi:

package main

import "fmt"

func oshir(son int) {
    son++
}

func main() {
    son := 10
    oshir(son)
    fmt.Println(son)
}

Natija:

10

oshir() ichidagi son boshqa o‘zgaruvchi. Uning o‘zgarishi main() ichidagi qiymatga ta’sir qilmaydi.

Funksiya asl qiymatni o‘zgartirishi kerak bo‘lsa, ko‘rsatkich uzatiladi:

package main

import "fmt"

func oshir(son *int) {
    *son++
}

func main() {
    son := 10
    oshir(&son)
    fmt.Println(son)
}

Natija:

11

oshir(&son) o‘zgaruvchining manzilini beradi. Funksiya ichidagi *son++ yozuvi (*son)++ bilan bir xil: avval manzildagi qiymat olinadi, keyin u bittaga oshiriladi.

Ko‘rsatkich yagona yechim emas. Funksiya yangi qiymat qaytarishi ham mumkin:

func oshir(son int) int {
    return son + 1
}

Faqat bitta sodda qiymat hisoblanayotgan bo‘lsa, natijani qaytarish ko‘pincha tushunarliroq. Ko‘rsatkich funksiya bir nechta maydonni o‘zgartirishi yoki mavjud obyekt ustida ishlashi kerak bo‘lganda qulay.

Struct bilan ko‘rsatkich ishlatish

Struct funksiyaga oddiy qiymat sifatida uzatilsa, barcha maydonlari nusxalanadi. Ko‘rsatkich orqali esa shu structning o‘zi o‘zgartiriladi:

package main

import "fmt"

type User struct {
    Name   string
    Active bool
}

func activate(user *User) {
    if user == nil {
        return
    }
    user.Active = true
}

func main() {
    user := User{Name: "Ali"}
    activate(&user)
    fmt.Println(user.Name, user.Active)
}

Natija:

Ali true

Struct maydoniga murojaat qilishda (*user).Active deb yozish mumkin. Go bu holat uchun qisqa user.Active yozuviga ruxsat beradi.

Structni ko‘rsatkich bilan yaratishning ikki keng tarqalgan ko‘rinishi bor:

user1 := &User{Name: "Ali"}
user2 := new(User)
user2.Name = "Vali"

Birinchi ko‘rinishda boshlang‘ich maydonlar darhol beriladi. new(User) esa barcha maydonlari nol qiymatda bo‘lgan *User qaytaradi.

Ko‘rsatkichni qaytarish

Funksiya ichida yaratilgan lokal qiymatga ko‘rsatkich qaytarish mumkin:

package main

import "fmt"

func yangiSon() *int {
    son := 42
    return &son
}

func main() {
    p := yangiSon()
    fmt.Println(*p)
}

Natija:

42

Funksiya tugagach son darhol yaroqsiz bo‘lib qolmaydi. Go kompilyatori qiymat qancha yashashi kerakligini aniqlaydi va xotirani boshqaradi. Dasturchi C tilidagidek xotirani qo‘lda bo‘shatmaydi.

Ko‘rsatkichlarning tengligi

Ikki ko‘rsatkich bir xil manzilni ko‘rsatsa, ular teng hisoblanadi:

package main

import "fmt"

func main() {
    x := 5
    p1 := &x
    p2 := &x
    y := 5
    p3 := &y

    fmt.Println(p1 == p2)
    fmt.Println(p1 == p3)
}

Natija:

true
false

x va y qiymatlari teng bo‘lsa ham, ularning manzillari boshqa-boshqa. Ko‘rsatkichni faqat boshqa shu turdagi ko‘rsatkich yoki nil bilan taqqoslash mumkin.

nil ko‘rsatkich

Ko‘rsatkichning nol qiymati nil, ya’ni u hech qanday yaroqli qiymatni ko‘rsatmaydi:

package main

import "fmt"

func qiymat(p *int) int {
    if p == nil {
        return 0
    }
    return *p
}

func main() {
    var p *int
    x := 42

    fmt.Println(qiymat(p))
    fmt.Println(qiymat(&x))
}

Natija:

0
42

nil ko‘rsatkichni *p bilan ochish runtime’da panic beradi. Funksiya nil qabul qilishi mumkin bo‘lsa, uni tekshiring yoki funksiya shartida nil taqiqlanganini aniq belgilang.

new funksiyasi

new(T) T turining nol qiymati uchun joy tayyorlab, *T qaytaradi:

package main

import "fmt"

func main() {
    p := new(int)
    fmt.Println(*p)

    *p = 42
    fmt.Println(*p)
}

Natija:

0
42

Ko‘pincha oddiy lokal o‘zgaruvchi va & tushunarliroq: x := 0; p := &x. new bilan make bir xil emas. new(T) *T qaytaradi; make esa faqat slice, map va channel’ning foydalanishga tayyor qiymatini qaytaradi.

Go’da pointer arifmetikasi yo‘q

Go odatiy kodda p++ yoki p + 1 kabi pointer arifmetikasiga ruxsat bermaydi. Bu xotira bilan ishlashni xavfsizroq qiladi. Past darajadagi maxsus vazifalar uchun unsafe paketi bor, lekin u odatiy biznes kodida kerak emas.

Ko‘rsatkich ishlatish qiymat albatta heap’da saqlanadi degani ham emas. Kompilyator escape analysis orqali qiymat stack’da qolishi yoki heap’ga ko‘chishini aniqlaydi. Shuning uchun pointer faqat “tezroq” degan taxmin bilan tanlanmaydi; avval semantika va o‘lchash muhim.

Slice, map va ko‘rsatkich

Slice va map funksiyaga uzatilganda ularning sarlavha qiymati nusxalanadi, lekin nusxa ham asosiy ma’lumotga murojaat qiladi. Elementni o‘zgartirish uchun odatda slice yoki mapga ko‘rsatkich kerak emas:

func birinchiElementniAlmashtir(sonlar []int) {
    if len(sonlar) > 0 {
        sonlar[0] = 100
    }
}

func qiymatQosh(lugat map[string]int) {
    lugat["go"] = 2009
}

Ammo funksiyada append() natijasida slice sarlavhasi o‘zgarsa, yangi slice’ni qaytarish kerak:

func qosh(sonlar []int, son int) []int {
    return append(sonlar, son)
}

Bu usul *[]int ishlatishdan odatda sodda. Ko‘rsatkichni faqat qiymatni o‘zgartirish zarurati aniq bo‘lganda tanlang.

Qachon ko‘rsatkich ishlatmaslik kerak?

Quyidagi holatlarda oddiy qiymat yetarli bo‘lishi mumkin:

  • funksiya argumentni faqat o‘qisa va qiymat kichik bo‘lsa;
  • yangi natijani qaytarish mavjud qiymatni o‘zgartirishdan tushunarliroq bo‘lsa;
  • nil holati dasturga ortiqcha murakkablik olib kirsa;
  • slice, map yoki channelning o‘z xususiyati kerakli murojaat semantikasini bersa.

Har bir argumentni ko‘rsatkichga aylantirish kodni avtomatik ravishda tezlashtirmaydi. Ko‘rsatkich umumiy ma’lumotni bir nechta joydan o‘zgartirish imkonini bergani uchun kod oqimini kuzatishni ham qiyinlashtirishi mumkin.

Xulosa

  • & manzilni oladi, * manzildagi qiymatga murojaat qiladi.
  • nil ko‘rsatkichni ochish panic beradi.
  • new(T) nol qiymatga ko‘rsatkich qaytaradi.
  • Go’da odatiy pointer arifmetikasi yo‘q.
  • Pointer asl qiymatni o‘zgartirish yoki nusxalashni boshqarish zarur bo‘lganda ishlatiladi.
  • Slice va map elementlarini o‘zgartirish uchun odatda alohida ko‘rsatkich kerak emas.

Keyingi darslarda ko‘rsatkichning funksiya argumentlari va metod receiverlari bilan ishlashi amalda ko‘riladi.