21 / 73

Metodlar va receiver'lar

Goda klasslar yo'q, lekin metodlar bor, va bu farq ko'rinadiganidan muhimroq. Godagi metod — bu shunchaki maxsus qo'shimcha parametrga, ya'ni receiver'ga ega funksiya bo'lib, u parametrlar ro'yxati ichida emas, funksiya nomidan oldin yoziladi. Yashirin this yo'q, mulohaza qilinadigan meros zanjiri yo'q va aniq tiplar uchun sahna ortida vtable dispetcherlash yo'q. Metodlarni yaxshi tushunish asosan har bir metod uchun bir marta qilinadigan bitta qarorga borib taqaladi: receiver value bo'lsinmi yoki pointer? Shu savolga ko'nikkaningizdan keyin Goning "obyektga yo'naltirilgan" kodga yondashuvining qolgan qismi — merosdan ko'ra kompozitsiya, implicit qanoatlantiriladigan kichik interface'lar — tabiiy ravishda kelib chiqadi.

Metodni e'lon qilish

Receiver func bilan metod nomi orasidagi alohida qavs ichidagi bo'limda turadi:

example.go
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

rect := Rectangle{Width: 10, Height: 5}
rect.Area() // 50

(r Rectangle) — receiver: r metod tanasi receiver qiymatga murojaat qilish uchun ishlatadigan lokal nom, Rectangle esa metod biriktirilgan tip. Metodlar struct'lar bilan cheklanmaydi — ularni o'z paketingizda aniqlangan har qanday nomlangan tipga, jumladan o'rnatilgan tipga asoslangan tipga ham biriktirish mumkin:

example.go
type Fahrenheit float64

func (f Fahrenheit) ToCelsius() Fahrenheit {
    return (f - 32) * 5 / 9
}

var temp Fahrenheit = 98.6
temp.ToCelsius() // 37

"Nomlangan tip" talabi muhim: metodni to'g'ridan-to'g'ri int, string yoki boshqa o'rnatilgan tipga biriktira olmaysiz, va boshqa paketda aniqlangan tipga ham metod biriktira olmaysiz — Go receiver tipi metod bilan bir paketda e'lon qilinishini talab qiladi, aynan shu sizga, aytaylik, time.Timega o'z metodlaringizni qo'shishga yo'l qo'ymaydi. Yuqoridagi Fahrenheit kabi aniqlangan tip ikkala cheklovni arzon aylanib o'tadi.

Value receiver va pointer receiver

Bu har bir metod e'loni sizni qaror qabul qilishga majbur qiladigan tanlov, va uni izchil to'g'ri qilish deyarli har qanday boshqa uslub tanlovidan ko'ra muhimroq.

Value receiver, (r Rectangle), metodga receiver'ning o'z nusxasini beradi. Undan maydonlarni o'qish siz kutgandek ishlaydi, lekin metod ichida maydonga qilingan har qanday o'zlashtirish faqat nusxaga tegadi — chaqiruvchining asl qiymatiga hech qanday ta'sir qilmaydi:

example.go
func (r Rectangle) Scale(factor float64) {
    r.Width *= factor  // NUSXAni o'zgartiradi, chaqiruvchining Rectangle'ini emas
    r.Height *= factor
}

rect := Rectangle{Width: 10, Height: 5}
rect.Scale(2)
fmt.Println(rect.Width) // hamon 10 — Scale chaqiruvchi ko'ra oladigan ta'sir qilmadi

Pointer receiver, (r *Rectangle), metodga chaqiruvchining haqiqiy qiymati manzilini beradi, shuning uchun o'zgartirishlar metod qaytgach chaqiruvchiga ko'rinadi:

example.go
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor  // ASL qiymatni pointer orqali o'zgartiradi
    r.Height *= factor
}

rect := Rectangle{Width: 10, Height: 5}
rect.Scale(2)
fmt.Println(rect.Width) // 20 — chaqiruvchining qiymati haqiqatan o'zgardi

Qaysi birini ishlatishni hal qiladigan qoida deyarli mexanik: agar metod receiver'ni o'zgartirishi kerak bo'lsa, u pointer receiver'dan foydalanishi shart — o'zgarishni chaqiruvchiga ko'rinadigan qilishning boshqa yo'li yo'q. Shu qat'iy talabdan tashqari, pointer receiver'lar katta struct'lar uchun ham to'g'ri tanlov, bunda har metod chaqiruvida nusxalash vaqt va xotirani isrof qiladi, value receiver'lar esa o'zgarmas ma'lumot sifatida qarashni istaydigan kichik, oddiy tiplar uchun to'g'ri standart — koordinatalar, pul miqdorlari, "nusxa" qiymatni uzatishning mutlaqo tabiiy va arzon usuli bo'lgan har qanday narsa.

Metod receiver'ni o'zgartirishi kerak bo'lsa, u pointer receiver'dan foydalanishi shart.

Bir tip ichida izchillik

Tipdagi birorta metod pointer receiver'ni talab qilgach, kuchli kelishuv shuki, faqat maydonlarni o'qiydigan va texnik jihatdan value receiver'dan foydalana oladigan metodlar ham shu tipdagi barcha metodlar pointer receiver'dan foydalanishi kerak:

example.go
type Counter struct {
    value int
}

func (c *Counter) Increment() { c.value++ }
func (c *Counter) Decrement() { c.value-- }
func (c *Counter) Value() int { return c.value } // faqat o'qisa ham pointer receiver

Buning sababi method set'ning yaxlit ekanligi bilan bog'liq, keyingi bo'limda ko'rib chiqiladi: bir tipda receiver turlarini aralashtirish shunday tip yaratadiki, uning value shakli va pointer shakli turlicha interface'lar to'plamini implementatsiya qiladi — bu nozik chalkashlik manbai bo'lib, har bir tip uchun yagona receiver turini tanlab, unga qat'iy rioya qilish orqali uning oldi oson olinadi.

Metodlarni chaqirish: Goning avtomatik manzil olishi

Go chaqiruv joyida value/pointer farqini yuqoridagi mexanika ko'rsatganidan ko'ra ancha qulaylashtiradi. Pointer receiver'li metodni addressable (manzili olinadigan) qiymatda — masalan, lokal o'zgaruvchida — chaqirsangiz, Go sizning o'rningizga uning manzilini avtomatik oladi:

example.go
rect := Rectangle{Width: 10, Height: 5}
rect.Scale(2)     // (&rect).Scale(2) ning qisqartmasi — Go & ni avtomatik qo'yadi

Va pointer orqali value receiver'li metodni chaqirish avtomatik dereferens qiladi:

example.go
p := &rect
p.Area() // (*p).Area() ning qisqartmasi

Bu qulaylikning bitta nozik jihati bor: u faqat kompilyator manzilni ola olgandagina ishlaydi, ya'ni qiymat addressable bo'lishi kerak — o'zgaruvchi, addressable struct'ning maydoni yoki addressable massivning elementi bo'lishi lozim. Masalan, map'da saqlangan qiymat yoki funksiya chaqiruvidan qaytgan vaqtinchalik natija addressable emas:

example.go
rects := map[string]Rectangle{"a": {Width: 1, Height: 2}}
// rects["a"].Scale(2) // kompilyatsiya xatosi: rects["a"] da pointer metodni chaqirib bo'lmaydi (map qiymati addressable emas)

Bu dasturchilar method set qoidalariga abstrakt nazariya sifatida emas, amaliy kompilyatsiya xatosi sifatida duch keladigan eng keng tarqalgan holatdir. Agar map elementlariga pointer receiver'li metodlar chaqirilishi kerak bo'lsa, map'da qiymatlarning o'zini emas, ularning pointer'larini saqlash (map[string]*Rectangle) eng idiomatik yechim hisoblanadi.

Method set'lar va interfeyslarni implementatsiya qilish

Tipning method set'i — shu tipdagi qiymatda chaqirish mumkin bo'lgan metodlarning to'liq ro'yxatidir. Go biror tip interface talablarini bajara oladimi-yo'qligini tekshirishda aynan method set'ga tayanadi. Qoida assimetrik va uni aniq eslab qolish juda muhim: T ning method set'i faqat value receiver (t T) bilan e'lon qilingan metodlarni o'z ichiga oladi, *T ning method set'i esa ham value-receiver, ham pointer-receiver metodlarni to'liq qamrab oladi. Boshqacha aytganda, tipga bo'lgan pointer oddiy qiymat qila oladigan barcha metodlarni chaqira oladi, plyus pointer-receiver metodlarni ham. Ammo oddiy qiymat pointer-receiver metodlarni interface orqali chaqira olmaydi, chunki kompilyator xotirada addressable bo'lmagan nusxa uchun xavfsiz manzil sintez qila olmaydi.

example.go
type Incrementer interface {
    Increment()
}

type Counter struct{ value int }

func (c *Counter) Increment() { c.value++ }

var i Incrementer = &Counter{} // to'g'ri — *Counter ning method set'ida Increment bor
// var i2 Incrementer = Counter{} // kompilyatsiya xatosi — Counter ning method set'ida Increment YO'Q

Bu yangi boshlovchilar tez-tez duch keladigan chalkash xatoning sababidir: metod struct'da e'lon qilingan bo'lsa ham, kompilyator does not implement Incrementer deb xato beradi. Yechim oddiy: interface kutilgan joyga qiymat nusxasi emas, pointer (&Counter{}) uzatiladi. Tipda o'zgaruvchan holat (state) mavjud bo'lsa, pointer receiver'larni standart qilib tanlash eng to'g'ri yo'ldir — shunda qaysi shakl qaysi interfeysni implementatsiya qilishini alohida eslab yurishga hojat qolmaydi.

T ning method set'i faqat value-receiver metodlarni, *T ning method set'i esa ham value-receiver, ham pointer-receiver metodlarni o'z ichiga oladi.

Aksincha, barcha metodlari value receiver bo'lgan tip interface'larni oddiy qiymat bilan ham, pointer bilan ham teng darajada implementatsiya qiladi, chunki *T ning method set'i T method set'ini to'liq o'z ichiga oladi:

example.go
type Point struct{ X, Y float64 }

func (p Point) String() string { return fmt.Sprintf("(%v, %v)", p.X, p.Y) }

var s fmt.Stringer = Point{1, 2}  // yaxshi
var s2 fmt.Stringer = &Point{1, 2} // bu ham yaxshi

Metodlarga qurilgan andozalar

String() stringni amalga oshirish tipni fmt.Println, fmt.Printf("%v", ...) va fmt paketi fmt.Stringer interface'ini qidiradigan boshqa har qanday joyda avtomatik ishlaydigan qiladi — bu tipdagi qiymatni har safar debug-print qilganingizda o'zini oqlaydigan kichik sarmoya:

example.go
type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}

fmt.Println(Person{Name: "Alice", Age: 30}) // "Alice (30)"

Method chaining — har bir o'zgartiruvchi metoddan receiver'ning o'zini qaytarish — Go fluent builder API'larni shunday amalga oshiradi, va u pointer receiver'larni talab qiladi, shunda o'zgartirishlar zanjir bo'ylab bir xil asosiy qiymatda to'planadi:

example.go
type Builder struct{ parts []string }

func (b *Builder) Add(part string) *Builder {
    b.parts = append(b.parts, part)
    return b
}

func (b *Builder) Build() string {
    return strings.Join(b.parts, " ")
}

result := (&Builder{}).Add("Hello").Add("World").Build()

Va metodni ikkita bog'liq, lekin alohida shaklda mustaqil funksiya qiymati sifatida chiqarib olish mumkin: method value (rect.Area) — muayyan nusxaga bog'langan va receiver argumentisiz chaqiriladigan, va method expression (Rectangle.Area) — bog'lanmagan, receiver'ni aniq birinchi argument sifatida qabul qiladigan:

example.go
rect := Rectangle{Width: 10, Height: 5}

areaFn := rect.Area       // func() float64 — rect ga bog'langan
areaFn()                  // 50

areaExpr := Rectangle.Area // func(Rectangle) float64 — bog'lanmagan
areaExpr(rect)             // 50

Method expression'lar ilova kodida kamdan-kam uchraydi, lekin "keyinroq uzatiladigan har qanday Rectangle uchun Area metodi, umumiy tarzda" ni operatsiyalar slice'iga yoki dispatch jadvaliga uzatish kerak bo'lganda haqiqatan foydali.

Generic metodlar

Yuqoridagi receiver'lar haqidagi hamma narsa generics rasmga kirishidan oldingi davrga tegishli, va Go 1.18 (generics kelgan payt) dan Go 1.26 gacha mavjud bo'lgan bo'shliqni aniq aytib o'tish foydali. Generic tip aniqlab, unga tipning o'z type parameter'laridan foydalanadigan metodlar berish mumkin edi:

example.go
type Container[T any] struct {
    items []T
}

func (c *Container[T]) Add(item T) {
    c.items = append(c.items, item)
}

Lekin metodning o'zi receiver tipi allaqachon olib yurgan narsadan tashqari yangi type parameter kiritolmasdi. Agar sizga boshqa tip bo'yicha generic bo'lishi kerak bo'lgan metod kerak bo'lsa — masalan, Container[T]ni Container[U]ga aylantirish — uni erkin funksiya sifatida yozishga majbur edingiz, chunki func (c *Container[T]) MapTo[U any](...) shunchaki yaroqli Go emasdi.

Go 1.27 bu bo'shliqni yopadi: metod e'lonlari endi receiver'da allaqachon mavjud type parameter'lardan mustaqil ravishda o'zining type parameter'larini e'lon qila oladi.

example.go
func (c *Container[T]) MapTo[U any](f func(T) U) *Container[U] {
    out := &Container[U]{items: make([]U, len(c.items))}
    for i, v := range c.items {
        out.items[i] = f(v)
    }
    return out
}

names := &Container[int]{items: []int{1, 2, 3}}
labels := names.MapTo(func(n int) string { return fmt.Sprintf("#%d", n) }) // *Container[string]

Bu generic receiver tiplari bilan ham cheklanmaydi — oddiy, generic bo'lmagan tipdagi metod ham endi o'z sharti bilan generic bo'lishi mumkin, aynan Go jamoasi o'zining rag'batlantiruvchi misoli sifatida ishlatgan shakl: math/rand/v2ning aniq *Rand tipidagi (*Rand) N[Int intType](n Int) Int kabi metod, bitta metodga har bir butun son tipi bilan o'zining type parameter'i orqali ishlash imkonini beradi, alohida erkin funksiya yozish yoki har bir chaqiruvchini int orqali va qo'lda o'tkazish orqali o'tkazish o'rniga.

Bu imkoniyatga murojaat qilishdan oldin bilish kerak bo'lgan ikkita muhim cheklov bor. Birinchidan, interface tipida e'lon qilingan metod o'zining shaxsiy type parameter'lariga ega bo'la olmaydi. Ikkinchidan, va bevosita shundan kelib chiqib: generic metod interface metodi talabini bajarish (implementatsiya qilish) uchun ishlatilmaydi, chunki interfeys orqali chaqirilganda qaysi aniq type argument nazarda tutilganini ko'rsatish imkoni yo'q. Generic metodlar aniq tiplar va generic tiplarning o'z metodlari uchun vosita bo'lib, interfeyslar nima talab qila olishini o'zgartirmaydi. Kitobning keyingi Generics bo'limida (29 dan 32 gacha) type parameter'lar va constraint'lar to'liq yoritiladi; bu yerda esa metodlarga xos jihatlar keltirildi.

Amaliy misol

Kichik bank tipi realistik metodlar to'plamida izchil qabul qilingan receiver qarorini ko'rsatadi — o'zgartiruvchi amallar (Deposit, Withdraw, Transfer) balans va tranzaksiyalar tarixini o'zgartirgani uchun pointer receiver oladi, faqat o'qiydigan amallar (Balance, Statement) texnik jihatdan value receiver ishlatishi mumkin bo'lsa ham, izchillik kelishuviga ko'ra ular ham pointer ishlatadi:

example.go
type Account struct {
    Number       string
    Balance      float64
    Transactions []Transaction
}

func (a *Account) Deposit(amount float64) error {
    if amount <= 0 {
        return errors.New("deposit amount must be positive")
    }
    a.Balance += amount
    a.Transactions = append(a.Transactions, Transaction{Type: "deposit", Amount: amount})
    return nil
}

func (a *Account) Withdraw(amount float64) error {
    if amount <= 0 {
        return errors.New("withdrawal amount must be positive")
    }
    if a.Balance < amount {
        return errors.New("insufficient funds")
    }
    a.Balance -= amount
    a.Transactions = append(a.Transactions, Transaction{Type: "withdrawal", Amount: amount})
    return nil
}

func (a *Account) Transfer(to *Account, amount float64) error {
    if err := a.Withdraw(amount); err != nil {
        return err
    }
    if err := to.Deposit(amount); err != nil {
        a.Balance += amount // deposit tomoni muvaffaqiyatsiz bo'lsa, yechishni bekor qilish
        return fmt.Errorf("transfer failed: %w", err)
    }
    return nil
}

Transferga yana bir qarash foydali: u ikki xil *Account qiymatida ikkita boshqa pointer-receiver metodni chaqiradi va deposit muvaffaqiyatsiz bo'lsa, yechishni aniq bekor qiladi — bir necha qadamli o'zgartirishlar bilan ishlaydigan haqiqiy kod qisman muvaffaqiyatsizlik haqida shunday o'ylashi kerak, chunki Goda tayanish uchun o'rnatilgan tranzaksiyalar yo'q.

Mashq

Receiver odatini shakllantirish uchun qisqa mashqlar to'plami: Increment, Decrement, Reset va Value metodli Counterni amalga oshiring (hammasi pointer receiver, izchillik qoidasiga ko'ra). Rectanglega Area, Perimeter va o'zgartiruvchi Scale metodini bering va har biriga qaysi receiver kerakligiga e'tibor bering. Slice maydoni bilan ta'minlangan stack'ni — Push, Pop, Peek, IsEmpty, Size — amalga oshiring, bu oldingi boblardagi map/slice-maydonni o'zgartirish andozalarini receiver qarori bilan birga mashq qilish uchun yaxshi majburlovchi vosita. Va yuqoridagi Account tipini TransactionHistory() []Transaction metodi bilan kengaytiring, u asosiy slice'ni to'g'ridan-to'g'ri qaytarishi kerakmi yoki himoya nusxasinimi, buni ongli ravishda hal qiling.

Metodlar — mexanizm; keyingi bob ularning ostida yotgan narsa haqida — Go xotirani aslida qanday boshqaradi, nega ba'zi qiymatlar stack'da yashaydi-yu, boshqalari heap'ga escape qiladi, va bu qaror hozirgina o'rgangan pointer'lar va receiver'lar haqidagi hamma narsa bilan qanday o'zaro ta'sir qiladi.