Go tili ommaga taqdim etilganidan so'ng 12 yildan ortiq vaqt davomida interface{} va type assertion'dan boshqa, funksiya yoki ma'lumot tuzilmalarini tiplar bo'yicha parametrlash imkoniyati bo'lmagan. Bu e'tiborsizlik emas, balki ataylab qilingan edi: Go jamoasi qulay va toza dizayn yaratish ustida yillar davomida bahslashdi va type parameter'larni faqat Go 1.18 versiyasida (2022-yil) rasman taqdim etdi.
Go 1.18 dan oldin "bir nechta har xil tiplar bilan bir xil mantiqda ishlash" kerak bo'lganda, dasturchilar oldida ikkita yo'l bor edi, va ikkalasining ham jiddiy kamchiliklari mavjud edi:
Birinchisi — kodni nusxalash (takrorlash). Agar int slaysidan ham, string slaysidan ham element qidirish kerak bo'lsa, findInt va findString funksiyalari alohida yozilardi — bitta tip nomidan tashqari butunlay bir xil kod:
func findInt(slice []int, value int) int {
for i, v := range slice {
if v == value {
return i
}
}
return -1
}
func findString(slice []string, value string) int {
for i, v := range slice {
if v == value {
return i
}
}
return -1
}
Bu nafaqat zerikarli, balki xatolarga moyil yondashuvdir: har bir o'zgarish yoki tuzatish barcha nusxalarda alohida qilinishi shart edi.
Ikkinchi yo'l — bo'sh interface{} (Go 1.18 dan any muqobil nomi) ishlatish edi:
func find(slice []interface{}, value interface{}) int {
for i, v := range slice {
if v == value {
return i
}
}
return -1
}
Bu ishlaydi, ammo barcha statik tip xavfsizligini yo'qqa chiqaradi. Qiymatlarni interfeysga o'rash (boxing) xotirada heap allocation'ga olib keladi. Qaytarilgan natijani ishlatish uchun esa yana xavfli type assertion talab qilinadi.
Generics bu muammoni to'liq hal qiladi: funksiya bir marta yoziladi, barcha mos tiplar bilan ishlaydi va kompilyator tip xavfsizligini to'liq nazorat qiladi:
func find[T comparable](slice []T, value T) int {
for i, v := range slice {
if v == value {
return i
}
}
return -1
}
index := find([]int{1, 2, 3}, 2) // T — int
index = find([]string{"a", "b"}, "b") // T — string
Bu yerda [T comparable] — type parameter (tip parametri) hisoblanadi. comparable esa type constraint (tip cheklovi) bo'lib, T tipi == va != tenglik tekshiruvini qo'llab-quvvatlashini kompilyatorga bildiradi. Agar tenglash mumkin bo'lmagan tip berilsa, dastur kompilyatsiya vaqtidayoq xato beradi.
Generics va interface'lar bir xil muammoni hal qiladi deb o'ylash vasvasali, chunki ikkalasi bitta kod bo'lagiga bir nechta tipni qayta ishlashga imkon beradi. Bunday emas. Farq nima o'zgarishida.
Interface'lar xatti-harakat bo'yicha abstraksiya qiladi. io.Writer faylga, tarmoq ulanishiga yoki xotiradagi buferga yozayotganiga e'tibor bermaydi — muhimi, uchalasi ham Write([]byte) (int, error)ga javob berishni biladi. Aniq tip interface chegarasida o'chiriladi; io.Writerga qarshi yozilgan kod uni tip tasdiqlashsiz tiklay olmaydi, va asosan xohlamasligi ham kerak.
Generics tipni abstraksiya qiladi, xatti-harakatni bir xil va tip ma'lumotini butun saqlaydi. Generic Stack[T] T int yoki User ekaniga qarab boshqacha narsa qilmaydi — push-va-pop mantig'i ikkala holda ham aynan bir xil. Faqat nima saqlanishi o'zgaradi, va butun maqsad shuki, kompilyator T nima ekanini kuzatib boradi, shuning uchun Stack[User]da Pop() User qaytaradi, qaytadan assertion qilish kerak bo'lgan any emas.
Boshqacha aytganda: umumiy shartnoma orqali turli ishlarni qiladigan bir nechta tip bo'lganda interface'ga murojaat qiling. Bir algoritm yoki bir ma'lumotlar tuzilmasi o'zi e'tibor bermaydigan tiplar bo'ylab bir xil ishlashi kerak bo'lganda generics'ga murojaat qiling — stack, to'plam (set), Map/Filter/Reduce yordamchisi, kesh.
Interface'lar xatti-harakat bo'yicha abstraksiya qiladi; generics tip bo'yicha, tip ma'lumotini saqlagan holda.
Ikkalasi raqobatlashmaydi, balki birikadi. Generic funksiya interface bilan cheklangan type parameter olishi mumkin, va generic tip interface qiymatlarini ushlab turishi mumkin:
type Processor[T any] interface {
Process(T) error
}
func processAll[T any](items []T, p Processor[T]) error {
for _, item := range items {
if err := p.Process(item); err != nil {
return err
}
}
return nil
}
Bu yerda processAll element tipi T bo'yicha generic, Processor[T] esa shu tipdagi xatti-harakatni tasvirlovchi oddiy interface bo'lib qoladi. Hech biri boshqasini almashtirmaydi; har biri yaxshi bajaradigan qismini bajaradi.
Keshlash yaxshi misol, chunki mantiq — qiymatni saqlash, uni TTL'dan keyin muddati o'tkazish, lock ostida chiqarib tashlash — nima keshlanayotganiga chindan bog'liq emas, lekin production tizimlari muqarrar ravishda bir nechta turli narsani keshlashi kerak: foydalanuvchi yozuvlari, sessiya tokenlari, renderlangan sahifa fragmentlari, hisoblangan ballar.
package cache
import (
"sync"
"time"
)
type item[V any] struct {
value V
expiresAt time.Time
}
// Cache — tip-xavfsiz, TTL'ga asoslangan kesh. K map kaliti sifatida ishlatilishi
// mumkin bo'lishi kerak, aynan comparable constraint shuni kafolatlaydi.
type Cache[K comparable, V any] struct {
mu sync.RWMutex
items map[K]item[V]
ttl time.Duration
}
func New[K comparable, V any](ttl time.Duration) *Cache[K, V] {
return &Cache[K, V]{
items: make(map[K]item[V]),
ttl: ttl,
}
}
func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = item[V]{value: value, expiresAt: time.Now().Add(c.ttl)}
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
it, ok := c.items[key]
if !ok || time.Now().After(it.expiresAt) {
var zero V
return zero, false
}
return it.value, true
}
Bir marta testlangan bitta implementatsiya endi kod bazasidagi har bir keshlash ehtiyojiga xizmat qiladi:
sessions := cache.New[string, Session](30 * time.Minute)
sessions.Set("sess_abc123", Session{UserID: 42})
users := cache.New[int, User](5 * time.Minute)
users.Set(42, User{ID: 42, Name: "Alice"})
if s, ok := sessions.Get("sess_abc123"); ok {
fmt.Println(s.UserID) // s — Session, assertion kerak emas
}
Type parameter'lar takrorlanishdan qochishdan tashqari bu yerda nima berganiga e'tibor bering: sessions.Get Session qiymatini to'g'ridan-to'g'ri qaytaradi. Assertion qilinadigan interface{} yo'q, mos kelmaydigan cast'dan ish vaqtidagi panic ehtimoli yo'q, va go vet hamda kompilyator ikkalasi ham har bir chaqiruv joyida kesh orqali aynan nima o'tayotganini tushunadi. Bu kombinatsiya — bitta implementatsiya, to'liq tip xavfsizligi, nol assertion — generics uchun haqiqiy dalil. Kitobning ushbu qismidagi qolgan hamma narsa buni qo'pollik bilan emas, yaxshi qilish haqida, chunki type parameter'lardan foydalanib, baribir almashtirgan takrorlangan versiyadan o'qish qiyinroq kod bilan tugash mutlaqo mumkin.
Keyingi bob generic funksiyalarni batafsilroq ko'rib chiqadi: type parameter ro'yxatlari qanday ishlaydi, kompilyator tip argumentlarini chaqiruv joylaridan qanday chiqaradi va bu chiqarish qayerda o'z chegarasiga yetadi.