32 / 73

Idiomatik generics

Go tili type parameter'larsiz 12 yildan ko'proq yashadi, va til madaniyati bugun ham ushbu an'anani saqlab qolgan: generics — bu muayyan, takrorlanuvchi muammolarni yechish vositasi, kod yozishning birlamchi va majburiy uslubi emas. Oldingi boblardagi mexanika — constraint'lar, instansiyalash, type inference'ni bilish shart, ammo yetarli emas. Eng muhim mahorat — mulohaza: qachon type parameter qo'llash to'g'ri tanlov bo'lishini va qachon oddiy interfeys yoki aniq tip ancha toza va qulayroq ekanini farqlay bilishdir.

Generics o'zini oqlaydigan joylar

Haqiqiy, strukturaviy takrorlanishni yo'q qilish — eng to'g'ri holatdir. Bir xil mantiqni faqat tipini o'zgartirib uchinchi marta yozayotgan bo'lsangiz, generics aynan shuning uchun yaratilgan:

example.go
func findInt(s []int, v int) int       { /* ... */ }
func findString(s []string, v string) int { /* ... */ }
func findFloat64(s []float64, v float64) int { /* ... */ }

// shunga aylanadi:
func find[T comparable](s []T, v T) int {
	for i, x := range s {
		if x == v {
			return i
		}
	}
	return -1
}

Gap shunchaki kamroq yozishda emas. Generics paydo bo'lishidan oldin bunday takrorlanish odatda har bir chaqiruv joyida interface{} va type assertion bilan "hal qilinardi", bu esa kompilyatsiya vaqtidagi tip xatosini ish vaqtidagi panic'ga aylantirardi. Generics takrorlanishni yo'qotadi va tip tekshiruvini kompilyatsiya vaqtida saqlab qoladi — 1.18 gacha bo'lgan muqobilga nisbatan haqiqiy yutuq shu.

Tip-xavfsiz konteynerlar — ikkinchi aniq holat. Stack, queue yoki set bitta izchil element tipini saqlashi kerak, chiqarishda esa har bir chaqiruvchini type assertion qilishga majburlamasligi kerak:

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

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

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

Generics'dan oldin bu []interface{} va har bir Popda type assertion (v.(int), v.(string)) edi. Bunday kod kompilyatsiya bo'ladi, lekin birinchi mos kelmagan tipda ish vaqtida panic qiladi. Generic konteyner esa bu xato sinfini shunchaki ehtimoldan uzoq emas, umuman imkonsiz qiladi.

Sonli va tartiblangan qiymatlar bilan ishlaydigan algoritmlar — saralash, min/max, clamping — uchinchi takrorlanuvchi holat. Go 1.21 dan boshlab standart kutubxonaning o'z cmp paketi umumiy constraint'ni to'g'ridan-to'g'ri taqdim etadi. Buni bilish foydali, chunki ko'p mavjud kod va hujjatlar hamon undan oldingi, tashqi golang.org/x/exp/constraints paketiga ishora qiladi:

example.go
import "cmp"

func Max[T cmp.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

cmp.Ordered bilan birga standart kutubxonaning slices va maps paketlari ham (ular ham 1.21 dan) generic slices.Sort, slices.Contains, maps.Keys va shunga o'xshash funksiyalarni beradi. Ular ilgari qo'lda yozilgan generic yordamchilar yoki uchinchi tomon kutubxonasini talab qilgan ishlarning ko'pini qoplaydi. O'zingizniki yozishdan oldin ulardan foydalanish odatda to'g'ri qaror: standart kutubxona versiyalari yaxshi sinalgan va o'quvchi ularni ehtimol allaqachon taniydi.

Generics kodni yomonlashtiradigan joylar

Faqat bitta tipdagi maydonni generic qilish — eng keng tarqalgan ortiqcha yechim. Bu "struct'da maydon bor" degan fikrni parametrlash uchun yetarli sabab deb hisoblashdir, holbuki maydon tipi aslida hech qachon o'zgarmaydi:

example.go
// Bu nega generic? Name har bir haqiqiy ishlatishda doim string.
type Person[T any] struct {
	Name T
}

// Shunchaki mana bu.
type Person struct {
	Name string
}

Type switch haqiqatan kerak bo'lgan joyda type parameter'dan foydalanish nozikroq xato, va u generics nima uchun mo'ljallanganini noto'g'ri tushunishdan kelib chiqadi. Type parameter'lar har bir instansiyalash uchun kompilyatsiya vaqtida tanlangan bitta tipni ifodalaydi. Ular type switch yoki interface dispatch kabi, ish vaqtida aniq tipga qarab o'zini boshqacha tutadigan funksiya bermaydi.

example.go
// Bu kompilyatsiya bo'ladi, lekin vositaga qarshi kurashmoqda: T har bir
// chaqiruv joyi uchun kompilyatsiya vaqtida qat'iy, va funksiya tipga xos
// biror narsa qilish uchun hamon ish vaqtidagi type switch'ga muhtoj —
// type parameter bu yerda hech narsa bermadi.
func process[T *User | *Admin](entity T) {
	switch v := any(entity).(type) {
	case *User:
		handleUser(v)
	case *Admin:
		handleAdmin(v)
	}
}

// Aslida kerak bo'lgan narsa: ish vaqtidagi polimorfizm, interface'lar
// aynan shu uchun.
type Entity interface {
	Process()
}

func process(e Entity) {
	e.Process()
}

[T any] qo'shishdan oldin foydali ajratuvchi savol: bu kod har bir tip uchun boshqacha amalga oshirilgan bir xil xatti-harakatga ega ko'plab turli tiplar bilan ishlashi kerakmi (interface'lar — ish vaqtidagi dispatch), yoki bir vaqtda faqat bitta tip bilan, har bir tip uchun aynan bir xil kod bilan ishlashi kerakmi (generics — kompilyatsiya vaqtidagi ixtisoslashtirish)? Bularni chalkashtirish — sintaksisni yaqinda o'rgangan odamlar yozgan kodda eng keng tarqalgan generics xatosi.

Interface'lar har bir tip uchun boshqacha xatti-harakat (ish vaqtidagi dispatch), generics — har bir tip uchun bir xil kod (kompilyatsiya vaqtidagi ixtisoslashtirish).

Ikkinchi ishlatish holati paydo bo'lmasdan turib generic qilish — eng kam sezilarli, lekin eng keng tarqalgan xato. "Keyinroq boshqa tip uchun kerak bo'lib qolar" deb type parameter qo'shish — bu kitob boshqa joylarda ham qarshi chiqadigan spekulyativ murakkablikning aynan o'zi. Haqiqiy takrorlanish paydo bo'lishini kuting: keyinroq aniq tipni umumlashtirish mexanik refaktoringga arziydi, muddatidan oldin qo'shilgan type parameter esa uni kutib turgan butun vaqt davomida kodning har bir o'quvchisiga cheksiz qimmatga tushadi.

Tanib olish foydali bo'lgan andozalar

Bir nechta generic shakl haqiqiy Go kod bazalarida shunchalik tez-tez uchraydiki, ularni bir qarashda, har safar nima qilishini qatorma-qator aniqlab o'tirmasdan tanib olishga o'rganish foydali.

Generic repository/store — map ustidagi yupqa, tip-xavfsiz o'ram. U xotiradagi store sifatida yoki haqiqiy ma'lumotlar bazasi bilan ishlaydigan narsa uchun qurilish bloki sifatida foydali:

example.go
type Repository[T any, ID comparable] struct {
	mu   sync.RWMutex
	data map[ID]T
}

func NewRepository[T any, ID comparable]() *Repository[T, ID] {
	return &Repository[T, ID]{data: make(map[ID]T)}
}

func (r *Repository[T, ID]) Save(id ID, item T) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.data[id] = item
}

func (r *Repository[T, ID]) Find(id ID) (T, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	item, ok := r.data[id]
	return item, ok
}

Bu yerda ikkita type parameter turli vazifalarni bajaradi: T — saqlanadigan qiymat tipi, ID esa map kaliti sifatida ishlatilgani uchun comparable bilan cheklangan (Tning o'zi comparable bo'lishi shart emas). Ikki parametrli generic tipni o'qiyotganda ularni bir-birining o'rnini bosuvchi deb qarash o'rniga, har bir parametr aslida nima bilan cheklangan va nima uchunligini so'rash foydali.

Repository[T, ID] kabi tip generic metod uchun ham tabiiy joy: Go 1.26 va 1.27 dan boshlab metod receiver'nikidan alohida o'zining type parameter'larini e'lon qila oladi. Bu aynan repository'ga kerak: hech qachon parametrlanmagan biror maydon tipi bo'yicha filtrlaydigan bir martalik FindBy metodi uchun. Bu imkoniyat aynan metodlarga tegishli va yana bir generic-tip andozasi emas; u 21-bobda, Metodlar va receiver'lar bobida yoritilgan.

Generic funksional yordamchilar — slice'lar ustidagi Map, Filter, Reduce — Goga yangi kelganlarga funksional tilni eng ko'p eslatadigan andoza. Go 1.21 ning slices paketi ularning eng keng tarqalganlarini (slices.Contains, slices.Index, slices.Sort) o'zingiz yozmasdan qoplaydi. Standart kutubxonada yo'qlari uchun:

example.go
func Map[T, U any](s []T, f func(T) U) []U {
	result := make([]U, len(s))
	for i, v := range s {
		result[i] = f(v)
	}
	return result
}

func Filter[T any](s []T, pred func(T) bool) []T {
	var result []T
	for _, v := range s {
		if pred(v) {
			result = append(result, v)
		}
	}
	return result
}

Bular haqiqatan foydali, va har bir loyihada qayta ixtiro qilmasdan, umumiy ichki paketda bir marta yozib qo'yish foydali. Lekin ularning ustiga butun funksional dasturlash asboblar to'plamini qurish vasvasasiga qarshi turing. Go idiomasi eng oddiy o'zgartirishdan boshqa hamma narsa uchun hamon aniq for sikli: uch daraja chuqur Map/Filter/Reduce zanjiri qisqaroq bo'lsa ham, ko'pchilik Go o'quvchilari uchun ekvivalent siklni kuzatishdan ko'ra qiyinroq.

Mulohaza qarori, qayta bayon qilingan

Ushbu bobdagi har bir andoza bir xil savolga kelib taqaladi, va uni instinkt bilan emas, ochiq so'rash kerak: bugun aniq tiplar bo'ylab haqiqiy, strukturaviy takrorlanish bormi? Gipotetik emas, "keyinroq boshqa tip uchun kerak bo'lishi mumkin" emas, balki aynan o'sha mantiq va boshqa tipga ega haqiqiy ikkinchi (yoki uchinchi) chaqiruv joyi bormi? Agar ha bo'lsa, type parameter juda ehtimol to'g'ri vosita, va aynan kerakli narsa allaqachon mavjud bo'lishi mumkinligi sababli avval standart kutubxonaning cmp, slices va maps paketlarini tekshirish kerak. Agar halol javob "hali yo'q" yoki "bunga har bir tip uchun boshqacha xatti-harakat kerak, turli tiplar uchun bir xil xatti-harakat emas" bo'lsa, yaxshiroq vosita — aniq tip yoki interface. Generics esa baribir ishlatishga qaror qilishdan oldin tushunish kerak bo'lgan abstraksiya qatlamini qo'shadi: bu kodning hali ishlatilmaydigan moslashuvchanligi uchun har bir kelajakdagi o'quvchi to'laydigan narx.

Generics hal bo'lgach, keyingi bob bu andozalarning har biri oxir-oqibat duch keladigan mavzuga o'tadi: Go muvaffaqiyatsizlikni qanday tasvirlaydi va qayta ishlaydi, va xatolarni exception emas, oddiy qiymat sifatida qarash idiomatik Go kodining shunchalik katta qismini nima uchun shakllantiradi.