31 / 73

Generic tiplar

Generic funksiyalar turli tiplar uchun takrorlanadigan algoritmlarni bitta yechimga jamlaydi. Generic tiplar esa takrorlanish muammosining ikkinchi yarmini hal qiladi: stack, queue, set, daraxt, kesh kabi ma'lumotlar tuzilmalarini saqlash va aylanib chiqish mantig'i ichida qanday qiymat turishidan qat'i nazar bir xil bo'ladi. Struct o'zining type parameter ro'yxatini olib yurishi mumkin, va unda aniqlangan har bir metod bu parametrni avtomatik meros qilib oladi — shu tufayli bu andoza shunchalik toza ishlaydi.

Generic struct'ni e'lon qilish

Type parameter ro'yxati tip nomidan keyin yoziladi va har bir metodning receiver'ida takrorlanadi (constraint'larsiz):

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

func NewStack[T any]() *Stack[T] {
    return &Stack[T]{items: make([]T, 0)}
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}
example.go
ints := NewStack[int]()
ints.Push(1)
ints.Push(2)
v, ok := ints.Pop() // v — int, assertion kerak emas

Pop metodiga alohida e'tibor bering: T qaytarib, qandaydir "bo'sh" qiymatga tayanish o'rniga (T, bool) juftligini qaytarish generic kodda "olib tashlaydigan hech narsa yo'q" degan ma'noni bildirishning idiomatik usuli. Aniq []int bilan ishlaganda bo'sh holat uchun -1 qaytarish vasvasasi bo'lishi mumkin. Lekin T noma'lum bo'lganda tipga bog'liq bo'lmagan bunday belgi yo'q: Tning nol qiymati (var zero T) saqlangan haqiqiy nol qiymatdan farq qilmaydi. Shuning uchun bu yerda boolean qaytarish uslub tanlovi emas, ixtiyoriy T uchun "qiymat yo'q" degan ma'noni ifodalashning yagona to'g'ri usuli.

Ixtiyoriy T uchun "qiymat yo'q" degan ma'noni ifodalashning yagona to'g'ri usuli — qo'shimcha boolean.

Stack[int] va Stack[string] instansiyalashdan keyin butunlay alohida tiplarga aylanadi: birini ikkinchisiga o'zlashtira olmaysiz, *Stack[int] kutilgan joyga *Stack[string] uzata olmaysiz, aniq T tanlamasdan ikkalasini bitta []Stack[T] ichida saqlay olmaysiz. Goda generics kompilyatsiya vaqtida hal qilinadi (Javadagi type erasure'dan farqli o'laroq), ish vaqtida bu tiplar uchun umumiy tasvir yo'q.

Bir nechta type parameter va comparable

Kalit va qiymat tushunchasi bor joyda bir nechta type parameter'li tuzilmalar ko'p uchraydi:

example.go
type Cache[K comparable, V any] struct {
    mu    sync.RWMutex
    items map[K]V
}

func NewCache[K comparable, V any]() *Cache[K, V] {
    return &Cache[K, V]{items: make(map[K]V)}
}

K uchun bu yerda comparable uslub uchun emas, aniq sabab bilan kerak: Goda map kalitlari generics bo'lmasa ham comparable bo'lishi shart, tamom. Struct ichidagi map[K]V constraint'ni majburlaydi: kompilyator any bilan cheklangan kalitli map e'lon qilishga yo'l qo'ymaydi, chunki any map kaliti bo'la olmaydigan tiplarni (slice, map, funksiya) ham o'z ichiga oladi. Bundan umumiy qoida chiqadi: type parameter'ga kerak bo'lgan constraint tipning implementatsiyasi u bilan aslida nima qilishiga qarab belgilanadi, ochiq API qanchalik "erkin" ko'rinishini xohlashingizga qarab emas. Constraint'ni comparabledan anyga bo'shatmoqchi bo'lsangiz, avval tana hamon kompilyatsiya bo'ladimi-yo'qmi tekshiring — odatda bo'lmaydi, kompilyator xatosi esa qaysi amal qattiqroq chegara talab qilganini aniq ko'rsatadi.

comparable constraint'ida yana bir e'tiborga loyiq nozik jihat bor. Go 1.20 dan boshlab comparableni == qo'llab-quvvatlaydigan har qanday tip, jumladan interface tiplari va ularni o'z ichiga olgan struct'lar ham qanoatlantiradi. Lekin bunday ikki qiymatni solishtirish ish vaqtida baribir panic qilishi mumkin — masalan, interface ichidagi dinamik tip aslida comparable bo'lmasa (interface slice'ni ushlab turgan holat). Kompilyator buni []int kabi statik tiplar uchun ushlaydi, lekin comparable emaslik interface ortida yashiringanda ushlay olmaydi. Bu tor chekka holat, ammo shu sababli comparableni mustahkam kafolat emas, ish vaqtidagi istisnosi bor, kompilyatsiya vaqtidagi constraint deb tushunish to'g'ri.

Faqat kalitni emas, qiymatni ham cheklash

Generic tipda kalit bo'lmagan parametrlar ham any va comparable bilan cheklanib qolmaydi. Constraint funksiya uchun nimani ifodalay olsa, struct uchun ham xuddi shuni ifodalaydi:

example.go
type SortedList[T cmp.Ordered] struct {
    items []T
}

func (l *SortedList[T]) Insert(v T) {
    i, _ := slices.BinarySearch(l.items, v)
    l.items = slices.Insert(l.items, i, v)
}

Bu yerda SortedList[T] faqat tartiblanadigan (orderable) tip bilan instansiyalanishi mumkin, evaziga implementatsiya < operatoridan erkin foydalanadi va standart kutubxonaning slices paketiga tayanadi. slices paketi o'zi ham generic va yaxshi bilish foydali: slices.Insert, slices.BinarySearch, slices.Sort, slices.Contains va boshqalar 2022–2023 yillarda odamlar qo'lda yozgan generic konteynerlarning katta qismini almashtiradi.

Interface asosidagi constraint'lar tiplarda ham funksiyalardagidek ishlaydi. Ular yordamida, masalan, payload'lari o'zini qanday serializatsiya qilishni bilishi talab qilinadigan generic event bus qurish mumkin:

example.go
type Encodable interface {
    Encode() ([]byte, error)
}

type Queue[T Encodable] struct {
    items []T
}

func (q *Queue[T]) Flush(w io.Writer) error {
    for _, item := range q.items {
        data, err := item.Encode()
        if err != nil {
            return err
        }
        if _, err := w.Write(data); err != nil {
            return err
        }
    }
    q.items = q.items[:0]
    return nil
}

O'ziga havola qiluvchi constraint'lar

Go 1.26 gacha constraint boshqa tiplarga erkin havola qila olardi, lekin o'zi cheklayotgan type parameter'ning o'ziga havola qila olmasdi. Bu "o'zi bilan qanday birikishni biladi" turidagi API'larning butun bir sinfini chiqarib tashlaydigan cheklov edi. Go 1.26 uni olib tashladi:

example.go
type Adder[A Adder[A]] interface {
    Add(A) A
}

func Sum[A Adder[A]](items []A) A {
    var total A
    for _, item := range items {
        total = total.Add(item)
    }
    return total
}

Adder[A]da A uchun constraint Adder[A]ning o'zi — ya'ni "Add metodi o'z tipini qabul qilib, o'z tipini qaytaradigan tip". 1.26 dan oldin bu shunchaki kompilyatsiya bo'lmasdi: tip tekshiruvchisi o'zi cheklayotgan tipga bog'liq constraint'ni hal qila olmasdi. Bu tor imkoniyat, lekin algebraik, o'zini o'zi tiplaydigan API'lar aynan shuni talab qiladi — masalan, har bir metodi o'zining aniq tipini qaytaradigan builder yoki faqat o'zi bilan birika oladigan songa o'xshash tip. Endi buning uchun anyga qaytib, tip xavfsizligidan voz kechish shart emas.

Nomlash, muqobil nomlar (type aliases) va API soddaligi

Bir harfli type parameter'lar (T, K, V, U) umumiy maqsadli konteynerlar (stack, map, queue) uchun qabul qilingan standart hisoblanadi (xuddi sikl hisoblagichida i kabi). Lekin struct uchta yoki undan ko'p parametrga ega bo'lsa yoki ularning vazifasi noaniq bo'lsa, to'liq nom berish o'zini oqlaydi:

example.go
type Repository[Entity any, ID comparable] struct {
    data map[ID]Entity
}

Repository[Entity, ID] kodi Repository[T, U] ga qaraganda ancha tushunarli: dasturchi har safar "qaysi biri ID edi?" deb bosh qotirishi shart emas.

Ko'p ishlatiladigan generic tiplar uchun type alias yaratish kodni ancha toza qiladi:

example.go
type UserCache = Cache[int, User]

var cache UserCache
cache.Set(1, User{ID: 1})

Bu defined type'dan (type UserCache Cache[int, User], = belgisiz) farq qiladi: defined type alohida yangi tip hosil qiladi va unga metodlar qaytadan biriktirilishi kerak bo'ladi. Alias esa shunchaki muqobil qulay nom beradi.

Go 1.24 dan boshlab alias'lar ham o'zining generic parametrlariga ega bo'lishi mumkin:

example.go
type Cached[T any] = Cache[string, T]

var products Cached[Product]

Bu yerda Cached[T] kaliti doim string bo'lgan, qiymati esa erkin T bo'lgan kesh uchun juda qulay qisqartmadir.

Generic tiplardan qachon foydalanmaslik kerak?

Type parameter qo'shish mumkin bo'lgan har qanday struct'ga generics qo'llash shart emas. Xato arxitektura belgisi deyarli har doim bir xil: agar struct amalda faqat bitta aniq tip bilan ishlatilsa va hech qanday kod takrorlanishi yo'qotilmayotgan bo'lsa, generic parametr — bu ortiqcha murakkablikdir.

example.go
// Keraksiz murakkablik — Name doim string
type Person[T any] struct {
    Name T
}

// Idiomatik va to'g'ri
type Person struct {
    Name string
}

Oltin qoida: agar loyihangizda ushbu struct kamida ikki xil turli tiplar bilan ishlatilishiga aniq ehtiyoj bo'lmasa, uni oddiy aniq tip sifatida yozing. Keyinchalik zarurat tug'ilganda struct'ni generic qilish juda oson refaktoring hisoblanadi. Keraksiz generics esa kodni o'qiyotgan har bir dasturchi uchun ortiqcha kognitiv yuk va tushunarsizlik tug'diradi.

Keyingi bob aynan shu mulohazalarni chuqurroq ko'rib chiqadi: generics qachon kodni yaxshilaydi va qachon yomonlashtiradi.