Go interface'lari mexanik jihatdan qanday ishlashini — strukturaviy qanoatlantirish, method set'lar, har bir interface qiymati ichidagi tip/qiymat juftligi — bilish ishning faqat yarmi. Ikkinchi yarmi — mulohaza: interface'ga umuman qachon murojaat qilish, uni qanchalik katta qilish, kod bazasining qayerida joylashishi va keyinroq unga bog'liq hamma narsani buzmasdan qanday o'zgartirish. Bu bob aynan shu mulohaza haqida bo'lib, yaxshi yozilgan Go kodida shu qadar izchil ko'rinadigan bir necha ko'rsatmani o'z ichiga oladi — ular deyarli jamiyat qonuniga aylangan.
Ushbu bobdan faqat bitta jumlani eslab qolsangiz, shu bo'lsin. Funksiya parametrlari interface'lar o'z o'rnini oqlaydigan joy; funksiyaning qaytariladigan qiymatlari odatda aniq bo'lgani yaxshiroq.
Funksiya parametrlari — interface'lar o'zini oqlaydigan joy; qaytariladigan qiymatlar odatda aniq tip bo'lgani yaxshiroq.
Interface parametrlarining foydasi moslashuvchanlik va testlanuvchanlikda bo'lib, ikkalasi oddiy kodda darhol ko'rinadi:
func process(r io.Reader) error {
data, err := io.ReadAll(r)
if err != nil {
return err
}
// ...
return nil
}
process(os.Stdin)
process(&bytes.Buffer{})
process(strings.NewReader("data"))
process(mockReader{}) // testda almashtirish arzimas
process baytlar standart kirishdan, tarmoq soketidan, xotiradagi buferdan yoki test double'dan kelishiga e'tibor bermaydi — u faqat biror narsaning talab bo'yicha baytlar ishlab chiqara olishiga e'tibor beradi. Agar process o'rniga *os.Fileni talab qilganda, bytes.Buffer yoki test fixture'ga ega har bir chaqiruvchi haqiqiy fayl orqali o'tishi va har bir test haqiqiy I/O talab qilishi kerak bo'lar edi.
Aniq qaytariladigan tiplar foydasi esa chaqiruvchi nima chaqira olishi emas, natija bilan nima qila olishi haqida. Solishtiring:
func newProcessor() *Processor {
return &Processor{}
}
p := newProcessor()
p.SpecificMethod() // mavjud — p aniq *Processor
func newProcessor() Processor { // bu yerda Processor — interface
return &processorImpl{}
}
p := newProcessor()
p.InterfaceMethod() // yaxshi
p.SpecificMethod() // kompilyatsiya xatosi — interface'ning bir qismi emas
Interface qaytarish chaqiruvchi oqilona xohlashi mumkin bo'lgan ma'lumotni bekorga tashlab yuboradi — aniq tip uni yaratgan funksiya ichida allaqachon to'liq ma'lum edi. Nozikroq narx ham bor: interface qaytariladigan tip chaqiruvchilar uchun nil haqida mulohaza yuritishni qiyinlashtiradi, chunki endi ular oldingi bobda muhokama qilingan tip/qiymat juftligi bilan ishlaydi, shunchaki nil yoki nil emas bo'lgan oddiy pointer bilan emas. Aniq qaytariladigan tiplar butun muammoni chetlab o'tadi.
Standart kutubxona bu andozaga tinimsiz rioya qiladi. os.Open *os.File qaytaradi, io.ReadWriteCloser emas, garchi *os.File bu interface'ni qanoatlantirsa ham. bufio.NewReader *bufio.Reader qaytaradi, io.Reader emas. Interface tiplari io.Copy(dst Writer, src Reader) kabi funksiyalarning parametrlari uchun mavjud — konstruktorlar qaytaradigan narsa uchun emas.
Kompozitsiyalangan misol andozani boshidan oxirigacha aniq qiladi:
type UserRepository interface {
FindByID(id string) (*User, error)
}
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
NewUserService interface qabul qiladi (shuning uchun uni production'da haqiqiy ma'lumotlar bazasiga asoslangan repository bilan, testlarda esa xotiradagi soxta bilan ulash mumkin) va aniq *UserService qaytaradi (shuning uchun chaqiruvchilar uning to'liq, aniq method set'ini bilvositaliksiz oladi).
Qoidani ortiqcha qo'llamaslik uchun ta'kidlash foydali bo'lgan bitta qonuniy istisno bor: ish vaqtida tanlangan bir nechta aloqasiz aniq tipdan birini chindan qaytarishi kerak bo'lgan funksiya (bir necha implementatsiyadan birini qaytarishi mumkin bo'lgan factory) interface qaytarishdan boshqa ilojiga ega emas. Bu haqiqiy va keng tarqalgan andoza (io.Pipe allaqachon aniq bo'lgan ikkita *io.PipeReader/*io.PipeWriter qiymatini qaytaradi, lekin plagin uslubidagi factory'lar ko'pincha interface qaytaradi). Ko'rsatma "hech qachon interface qaytarmang" emas; u "aniq tip chaqiruvchiga ko'proq narsani aytadigan bo'lsa, shunchaki qila olgani uchun interface qaytarmang" degan ma'noni anglatadi.
Standart kutubxonaning eng ko'p ishlatiladigan interface'lari deyarli hammasi bir metodli: io.Reader, io.Writer, io.Closer, fmt.Stringer, error; sort.Interfacening uchta metodi eng katta keng tarqalgan istisno. Amalda yaxshi ishlaydigan taxminiy qoida: bir dan uch metodgacha qulay, to'rt-besh ikkinchi qarash foydali, va undan kattasi ehtimol tasodifan birlashtirilgan bir nechta interface'dir.
Sabab estetik emas, balki amaliy. Kichik interface muhim bo'lgan har bir o'q bo'yicha arzon:
Kompozitsiya standart kutubxonaning "lekin ba'zan menga bir metoddan ko'proq kerak" savoliga haqiqiy javobi:
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}
Bu uchta metod signaturasini ReadWriteCloserga to'g'ridan-to'g'ri qo'lda yozishdan afzal, ixchamlikdan tashqari sabab bilan ham: u o'z-o'zicha io.Readerni qanoatlantiradigan har qanday narsa ReadWriteCloserni qanoatlantirishning uchdan bir qismiga yetganini aniq qiladi va boshqa kodga, agar unga faqat shu kerak bo'lsa, doim mavjud eng keng interface'ga emas, faqat io.Reader qismiga bog'liq bo'lishga imkon beradi.
Ayniqsa interface'lar va ularning implementatsiyalari birga e'lon qilinadigan tillardan kelgan odamlar orasida takrorlanadigan xato — interface'ni asosiy implementatsiyasi bilan bir paketda aniqlash:
// package storage
type File struct{ /* ... */ }
type Reader interface {
Read([]byte) (int, error)
}
func (f *File) Read(p []byte) (int, error) { /* ... */ }
Bunda sintaktik jihatdan noto'g'ri narsa yo'q, lekin u qiymat yaratiladigan joy bilan iste'mol qilinadigan joyni teskarisiga aylantiradi. Godagi interface'ning butun maqsadi chaqiruvchiga nima kerakligini tasvirlash bo'lib, chaqiruvchi ko'pincha implementatsiyadan boshqa paketda — ba'zan hali mavjud bo'lmagan paketda joylashadi. Idiomatik Go tor interface'larni ularni iste'mol qiladigan funksiya yoki struct yonida aniqlashga moyil:
// package processor
type reader interface {
Read([]byte) (int, error)
}
func process(r reader) error { /* ... */ }
Endi process faqat u haqiqatan chaqiradigan bitta metodga bog'liq, va istalgan paketdagi har qanday tip — storage.File, bytes.Buffer yoki besh qatorli test soxta — processor va storage bir-birini bilishi shart bo'lmagan holda readerni qanoatlantiradi. Yaxshi tashkil etilgan Go kodidagi ko'plab bir metodli interface'lar nega eksport qilinmagan va mahalliy doirada ekanining sababi ham shu: ular paketning ochiq API'sining bir qismi emas, bir necha xil kirishni qabul qilishi kerak bo'lgan bitta funksiyaning implementatsiya tafsiloti.
Amaliy natija: interface'larni faraz asosida loyihalamang. Yangi kodni aniq tiplar bilan boshlang, va interface'ni ikkinchi implementatsiya yoki double talab qiladigan test yoki xatti-harakatni almashtirishning haqiqiy zarurati paydo bo'lgan aniq daqiqada kiriting — undan oldin emas. Bitta implementatsiyaga ega va hech qachon bittadan ortiq bo'lmaydigan interface — sof marosim; u bilvositalik qatlamini, kodni o'qiyotganda qidiriladigan qo'shimcha nomni qo'shadi, boshqa hech narsa emas.
Paket chiqarilgach va boshqa kod u eksport qiladigan interface'ga bog'liq bo'lgach, bu interface amalda muzlab qoladi. Unga metod qo'shish har bir mavjud amalga oshiruvchi uchun buzuvchi o'zgarish, chunki avval interface'ni qanoatlantirgan tip endi qanoatlantirmaydi:
// v1 — Process metodiga ega har kim buni qanoatlantiradi
type Processor interface {
Process(data []byte) error
}
// v2 — Processor'ning har bir mavjud implementatsiyasi endi kompilyatsiya bo'lmaydi
type Processor interface {
Process(data []byte) error
Validate(data []byte) error
}
Bu struct'ga maydon qo'shishning (odatda zararsiz) yoki aniq tipga metod qo'shishning (doim zararsiz) teskari tomoni — interface'lar Go tiplar tizimida "qo'shish" qo'shimcha emas, balki buzuvchi o'zgarish bo'lgan yagona joy, aynan qanoatlantirish strukturaviy va avtomatik bo'lgani uchun. Standart kutubxonaning o'z tarixida taniqli misol bor: io.ReaderFrom, io.WriterTo va shunga o'xshash interface'lar aynan shu sababli io.Readerga qo'shimcha sifatida emas, doim yangi, alohida interface sifatida kiritilgan.
Interface'ga metod qo'shish — buzuvchi o'zgarish: avval uni qanoatlantirgan tip endi qanoatlantirmaydi.
Amaliy javob — o'zgartirish orqali emas, kompozitsiya orqali kengaytirish:
// asl interface'ga tegmang
type Processor interface {
Process(data []byte) error
}
// imkoniyatni yangi, kengroq interface orqali qo'shing
type ValidatingProcessor interface {
Processor
Validate(data []byte) error
}
Processorning mavjud amalga oshiruvchilari kompilyatsiya bo'lishda davom etadi. Validatsiya kerak bo'lgan yangi kod ValidatingProcessorga bog'liq bo'lishi yoki uni imkoniyat bo'yicha tekshirish uchun tip tasdiqlashdan foydalanishi mumkin:
func run(p Processor) error {
if vp, ok := p.(ValidatingProcessor); ok {
if err := vp.Validate(nil); err != nil {
return err
}
}
return p.Process(nil)
}
Bu "ixtiyoriy interface" andozasi — tip tasdiqlash bilan qiymat qandaydir boyroq interface'ni ham qanoatlantiradimi-yo'qmi tekshirish va qanoatlantirmasa, chiroyli tarzda orqaga qaytish — standart kutubxona hech kimni buzmasdan ixtiyoriy imkoniyatlar qo'shishning aynan usuli: io.Copy oddiy o'qish/yozish siklga qaytishdan oldin manba io.WriterToni yoki maqsad io.ReaderFromni qanoatlantirishini tekshiradi, bu manba kodiga qaramaguningizcha ko'rinmaydigan sof unumdorlik optimallashtirishi.
Bobning ko'rsatmasini kod yozayotganda aslida qo'llashingiz mumkin bo'lgan shaklga jamlash: