45 / 68

Generics (umumlashtirilgan dasturlash)

Nima uchun generics kerak?

Faraz qilaylik, int slaysining yig'indisini hisoblovchi funksiya yozdik:

example.go
func SumInts(numbers []int) int {
    var total int
    for _, n := range numbers {
        total += n
    }
    return total
}

Endi float64 slaysi uchun ham xuddi shunday funksiya kerak bo'lsa, kodni deyarli so'zma-so'z takrorlashga to'g'ri keladi:

example.go
func SumFloats(numbers []float64) float64 {
    var total float64
    for _, n := range numbers {
        total += n
    }
    return total
}

Bu — kodni takrorlash muammosi. Go 1.18 versiyasidan boshlab, standart kutubxonaga qo'shilgan generics (umumlashtirilgan dasturlash) imkoniyati aynan shu muammoni hal qiladi: bitta funksiyani istalgan mos turdagi ma'lumot bilan ishlaydigan qilib yozish mumkin.


1. Birinchi generic funksiya

example.go
package main

import "fmt"

// [T int | float64] — T o'rniga int YOKI float64 kelishi mumkinligini bildiradi
func Sum[T int | float64](numbers []T) T {
    var total T
    for _, n := range numbers {
        total += n
    }
    return total
}

func main() {
    fmt.Println(Sum([]int{1, 2, 3}))
    fmt.Println(Sum([]float64{1.5, 2.5}))
}

Natija:

kod
6
4

[T int | float64] — bu type parameter (tur parametri) e'loni: T — funksiya ichida ishlatiladigan "noma'lum tur" nomi, int | float64 esa T o'rniga qaysi turlar kelishi mumkinligini cheklovchi constraint (cheklov). Go, funksiya qanday chaqirilishiga qarab, Tning haqiqiy turini avtomatik aniqlaydi — uni qo'lda ko'rsatishning hojati yo'q.


2. Bir nechta turdosh cheklov: taqqoslash mumkin bo'lgan qiymatlar

example.go
package main

import "fmt"

func Max[T int | float64 | string](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Max(3, 7))          // int bilan
    fmt.Println(Max("olma", "uzum")) // string bilan
}

Natija:

kod
7
uzum

Bitta Max() funksiyasi endi ham sonlar, ham satrlar bilan ishlay oladi — chunki > operatori int, float64 va string uchun bir xilda ma'noga ega (satrlar uchun alifbo tartibida solishtirish).


3. Cheklovni alohida interface sifatida e'lon qilish

Agar bir xil cheklov bir nechta funksiyada ishlatilishi kerak bo'lsa, uni alohida nom bilan e'lon qilish qulayroq:

example.go
package main

import "fmt"

// Number — cheklov: T faqat shu uchta turdan biri bo'lishi mumkin
type Number interface {
    int | int64 | float64
}

func SumConstraint[T Number](numbers []T) T {
    var total T
    for _, n := range numbers {
        total += n
    }
    return total
}

func main() {
    fmt.Println(SumConstraint([]int64{10, 20, 30}))
}

Natija:

kod
60

[!NOTE] Standart kutubxonaning slices va maps paketlari (oldingi darsda ko'rgan edik) aynan shu tarzda, generics yordamida yozilgan — shuning uchun ular istalgan turdagi slays/map bilan bir xil funksiya nomi ostida ishlay oladi.


4. Generic turlar: umumlashtirilgan struct

Nafaqat funksiyalar, balki turlar (struct) ham generic bo'lishi mumkin. Masalan, istalgan turdagi ma'lumot bilan ishlaydigan "Stack" (stek — LIFO tuzilma):

example.go
package main

import "fmt"

// Stack[T any] — T istalgan tur bo'lishi mumkin (hech qanday cheklov yo'q)
type Stack[T any] struct {
    items []T
}

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

func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    last := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return last, true
}

func main() {
    var s Stack[string] // string turidagi Stack yaratamiz

    s.Push("birinchi")
    s.Push("ikkinchi")

    val, ok := s.Pop()
    fmt.Println(val, ok)
}

Natija:

kod
ikkinchi true

any — interface{}ning qisqartmasi bo'lib, "istalgan tur" degan ma'noni bildiradi (bo'sh interfeys darsida ko'rgan edik). Bu yerda Stack[string] deb e'lon qilib, Tning aynan string bo'lishini aniq belgiladik — endi bu Stack faqat string qiymatlarni qabul qiladi, boshqa tur bilan ishlatishga urinsangiz, kompilyatsiya xatosi olasiz.


5. Amaliy misol: Map() funksiyasi

Boshqa ko'plab tillarda mashhur bo'lgan "har bir elementni o'zgartirib, yangi ro'yxat hosil qilish" (map) patternini Goda generics yordamida universal yozish mumkin:

example.go
package main

import "fmt"

// T — kirish elementlarining turi, U — natija elementlarining turi (ikkalasi HAR XIL bo'lishi mumkin)
func Map[T, U any](items []T, f func(T) U) []U {
    result := make([]U, len(items))
    for i, item := range items {
        result[i] = f(item)
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4}

    doubled := Map(nums, func(n int) int { return n * 2 })
    fmt.Println(doubled)

    // T = int, U = string — ikkala tur ham har xil!
    strs := Map(nums, func(n int) string { return fmt.Sprintf("son-%d", n) })
    fmt.Println(strs)
}

Natija:

kod
[2 4 6 8]
[son-1 son-2 son-3 son-4]

Bitta Map() funksiyasi orqali []intdan yangi []int ham, []intdan butunlay boshqa tur — []string ham hosil qilish mumkin bo'ldi.


Xulosa: qachon generics ishlatiladi?

HolatGenerics kerakmi?
Funksiya faqat bitta aniq tur bilan ishlaydiYo'q, oddiy funksiya yetarli
Bir xil mantiq bir nechta turda takrorlanmoqda (SumInts, SumFloats, ...)Ha, generic funksiyaga birlashtiring
Umumiy ma'lumot tuzilmasi (Stack, Queue, Set) yaratish kerakHa, generic struct
Cheklov bir nechta joyda ishlatiladiAlohida interface sifatida e'lon qiling

[!IMPORTANT] Generics — kuchli vosita, lekin har doim ham kerak emas. Agar funksiyangiz faqat bitta tur bilan ishlaydigan bo'lib qolaversa, uni generic qilib murakkablashtirishning hojati yo'q. Go jamoasining o'zi ham "generics'ni faqat haqiqiy takrorlanish muammosini hal qilish uchun ishlating, oldindan "ehtiyot shart" deb hamma joyga qo'ymang" deb tavsiya qiladi.