Nima uchun generics kerak?
Faraz qilaylik, int slaysining yig'indisini hisoblovchi funksiya yozdik:
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:
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.
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:
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.
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:
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).
interface sifatida e'lon qilishAgar bir xil cheklov bir nechta funksiyada ishlatilishi kerak bo'lsa, uni alohida nom bilan e'lon qilish qulayroq:
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:
60
[!NOTE] Standart kutubxonaning
slicesvamapspaketlari (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.
structNafaqat funksiyalar, balki turlar (struct) ham generic bo'lishi mumkin. Masalan, istalgan turdagi ma'lumot bilan ishlaydigan "Stack" (stek — LIFO tuzilma):
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:
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.
Map() funksiyasiBoshqa ko'plab tillarda mashhur bo'lgan "har bir elementni o'zgartirib, yangi ro'yxat hosil qilish" (map) patternini Goda generics yordamida universal yozish mumkin:
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:
[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.
| Holat | Generics kerakmi? |
|---|---|
| Funksiya faqat bitta aniq tur bilan ishlaydi | Yo'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 kerak | Ha, generic struct |
| Cheklov bir nechta joyda ishlatiladi | Alohida 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.