Statik tipli har qanday til bir xil savolga javob berishi kerak: kod bo'lagi bir necha xil qiymat bilan ishlay olishini qanday e'lon qiladi? C++ va Java unga klass ierarxiyalari va aniq implements bandlari bilan javob beradi. Go esa boshqacha javob beradi, va bu farq idiomatik Goga o'ziga xos shaklini beradigan narsalardan biri. Godagi interface — metod signaturalari to'plamidan boshqa narsa emas, va aynan shu metodlarga ega bo'lgan har qanday tip interface'ni avtomatik qanoatlantiradi — alohida e'lon yo'q, ro'yxatdan o'tkazish yo'q, interface paketini import qilish talab qilinmaydi. Bu yagona dizayn qarori Go dasturchilari paketlarni qanday tuzishi, testlarni qanday yozishi va bog'liqliklar haqida qanday o'ylashiga ta'sir qiladi, va o'zingizning interface'laringizni yozishdan oldin uni to'liq tushunish foydali.
Godagi interface — metod signaturalari to'plami; shu metodlarga ega har qanday tip uni avtomatik qanoatlantiradi.
Mumkin bo'lgan eng oddiy interface'dan boshlang:
type Writer interface {
Write([]byte) (int, error)
}
Bu Writer nima ekani haqida hech narsa demaydi — faqat u nima qila olishini aytadi. Istalgan paketdagi, istalgan kishi tomonidan, istalgan vaqtda yozilgan har qanday tip aynan shu signaturali metodga ega bo'lgan zahoti Writerni qanoatlantiradi:
type File struct {
// ...
}
func (f *File) Write(data []byte) (int, error) {
// ma'lumotni asosiy fayl deskriptoriga yozish
return len(data), nil
}
var w Writer = &File{}
File e'lonida Writer tilga olinmaydi. Kompilyator munosabatni strukturaviy tarzda, method set'larni solishtirish orqali, *Fileni Writer sifatida ishlatmoqchi bo'lgan nuqtada topadi. Buni odatda strukturaviy tiplashtirish (structural typing) deyishadi (Java yoki C#'ning nominal tiplashtirishidan farqli, u yerda klass Writer sifatida qaralishi uchun aniq implements Writer deb e'lon qilishi kerak), va ba'zan eski Python iborasi "duck typing" bilan ta'riflanadi — agar u o'rdakdek yursa va o'rdakdek qaqillasa, uni o'rdak deb hisoblang. Go kompilyatorining versiyasi Pythonnikidan qattiqroq, chunki u metod signaturalarini kompilyatsiya paytida tekshiradi; faqat munosabatni oldindan e'lon qilishingiz shart emas.
Amaliy foydasi katta: siz o'z paketingizda aynan sizga kerak bo'lgan xatti-harakatni tasvirlaydigan interface aniqlashingiz mumkin, va har qanday tip — standart kutubxonadagi yoki siz nazorat qilmaydigan uchinchi tomon modulidagi tiplar ham — to'g'ri metodlarga ega bo'lsa, uni avtomatik qanoatlantiradi. os.File muallifidan uni o'zingizning tor Writer interface'ingiz sifatida ko'rish uchun ruxsat kerak emas. Odamlar Go interface'lari ishlab chiqaruvchi tomonidan emas, iste'molchi tomonidan aniqlanadi deganda aynan shuni nazarda tutadi: qiymatni ishlatadigan kod unga qanday shakl kerakligini hal qiladi, tip muallifi u qanoatlantirishi so'ralishi mumkin bo'lgan har bir interface'ni oldindan bashorat qilishi shart emas.
Bitta aniq tip, va odatda shunday bo'ladi, bir vaqtning o'zida ko'plab interface'larni qanoatlantiradi, chunki interface'lar erkin ustma-ust tushadi:
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}
func (p Person) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Name string `json:"name"`
Age int `json:"age"`
}{p.Name, p.Age})
}
Person String() string metodiga ega bo'lgani uchun fmt.Stringerni qanoatlantiradi, va alohida MarshalJSON() ([]byte, error) metodiga ega bo'lgani uchun json.Marshalerni qanoatlantiradi. Ikkala interface bir-biri haqida bilmaydi, va Personga ularni amalga oshirishi aytilishi shart emas — fmt.Println(p) va json.Marshal(p) har biri ichkarida tip tasdiqlash (type assertion) orqali tegishli metodni o'zi topadi.
Buni tip faqat o'z method set'iga qarab mos keladigan yoki kelmaydigan ko'plab kichik interface "shakllari" sifatida tasavvur qilish mumkin:
*File
┌───────────────┐
│ Read() │───────┐
│ Write() │───┐ │
│ Close() │ │ │
└───────────────┘ │ │
│ │
Writer │ │
┌───────────────┐ │ │
│ Write() │◄───┘ │
└───────────────┘ │
│
ReadWriter │
┌───────────────┐ │
│ Read() │◄────────┘
│ Write() │◄─────── *File buni ham qanoatlantiradi
└───────────────┘
*Buffer, *Conn va mos Write metodiga ega har qanday boshqa tip
ham Writer'ni qanoatlantiradi — ro'yxat yo'q, ulashilgan
umumiy tip yo'q.
Standart kutubxonaning eng ta'sirli ikkita interface'i ham bitta metodli:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Goda baytlarni ko'chiradigan deyarli hamma narsa — fayllar, tarmoq ulanishlari, xotiradagi buferlar, kompressorlar, xesh funksiyalari, TLS oqimlari — bittasini yoki ikkalasini qanoatlantiradi. Bu standart kutubxona tarixining tasodifi emas; bu interface'larni kichik saqlashning bevosita foydasi. Bir metodli interface'ni amalga oshirish deyarli bepul (to'g'ri metodga ega har qanday tip, muallifi io.Reader haqida hech eshitmagan bo'lsa ham, uni allaqachon qanoatlantiradi), va uni testda soxtalashtirish arzimas. O'n ikki metodli interface esa har bir amalga oshiruvchini o'ziga kerak bo'lmasligi mumkin bo'lgan o'n bitta metod yozishga majburlaydi, va har bir test double'ni hech qachon chaqirmaydigan o'n bitta metodni stub qilishga majburlaydi.
Bitta metoddan ko'proq xatti-harakat kerak bo'lganda, Goning javobi kattaroq interface emas, balki kompozitsiya:
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
io.Closer
}
Bir interface'ni boshqasi ichiga embedding qilish ularning method set'larini shunchaki birlashtiradi. ReadWriteCloserni Read, Write va Close metodlariga ega har qanday tip qanoatlantiradi — bu metodlar turlicha nomlangan interface'lar orasida tarqalganmi yoki yo'qmi ahamiyatsiz; kompilyator faqat hosil bo'lgan yassi method set'ga qaraydi. Aynan shu sababli standart kutubxonada har bir funksiya aynan kerakli shaklga birikadigan kichik interface'lar alifbosi (Reader, Writer, Closer, Seeker, ReaderAt, …) bor, hamma narsa to'liq amalga oshirishi kerak bo'lgan yagona monolit Filega o'xshash interface o'rniga.
anyGodagi har bir tip nol metodli interface'ni qanoatlantiradi:
var x interface{} = 42
var y interface{} = "hello"
var z interface{} = []int{1, 2, 3}
Go 1.18 dan beri any — interface{} uchun oldindan e'lon qilingan taxallus bo'lib, bugungi idiomatik kod bo'sh interface kerak bo'lgan deyarli hamma joyda any yozadi — interface{} hamon ishlaydi va uni 1.18 dan oldin yozilgan kodda ko'rasiz, lekin yangi kodda unga ustunlik berishga sabab yo'q. Ikkala yozuv kompilyator uchun bir xil ma'noni beradi: hech qanday kompilyatsiya vaqtidagi xatti-harakat kafolatini olib yurmaydigan qiymat. any — fmt.Println argumentlarining tipi, ixtiyoriy JSON'ni dekodlayotganda map[string]anyning tipi va shakli noma'lum qiymatlarni chindan qabul qilishi kerak bo'lgan har qanday boshqa narsaning tipi.
anyning narxi shuki, u statik tip tekshiruvini chetlab o'tadi. Qiymat any sifatida qutiga solingach, kompilyator u bilan nima qilayotganingizni endi tekshira olmaydi; har bir amal ish vaqtida tekshiriladigan tip tasdiqlash yoki type switch orqali o'tishi kerak, va har bir xato kompilyatsiya xatosi o'rniga panic'ga aylanadi. Generics Go 1.18 da paydo bo'lishidan oldin any (yoki interface{}) shuningdek bir nechta tip bilan ishlaydigan funksiya yozishning yagona yo'li edi — sort.Interface, eski container/list va son-sanoqsiz qo'lda yozilgan "generic" konteynerlar hammasi unga tayanardi. Bugun, agar sizga aslida "bir nechta aniq tip ustida ishlaydigan, to'liq kompilyatsiya vaqtidagi tip xavfsizligi bilan bir xil mantiq" kerak bo'lsa, type parameter deyarli har doim yaxshiroq vosita; anyga esa tipi ish vaqtigacha noma'lum qiymatlarni chindan qayta ishlash kerak bo'lganda — tipsiz JSON'ni dekodlash, fmt.Stringer uslubidagi formatlashni amalga oshirish yoki generics'dan oldingi kodda generic konteynerni eski usulda qurish — murojaat qiling.
Bu Go interface dizaynining yangi boshlovchilarni eng ko'p qoqintiradigan qismi, va uni qoidani yodlash o'rniga aniq aqliy model qurish foydali. Interface qiymati — juftlik: tip va shu tipdagi qiymat. Yozganingizda
var w Writer
ikkala yarmi ham bo'sh interface qiymatini olasiz — tip yo'q, qiymat yo'q. w == nil aynan shuni (va faqat shuni) tekshiradi. Aniq qiymatni interface o'zgaruvchisiga o'zlashtirgan zahoti, aniq qiymatning o'zi nil bo'lsa ham, ikkala yarmi to'ladi:
var f *File = nil
var w Writer = f
// w nil EMAS — u tipga (*File) va qiymatga (nil *File pointer) ega
w nil interface emas. U (*File, nil) juftligini ushlab turgan interface. Bu e'tibordan chetda qoldirsa bo'ladigan chekka holat emas; bu Go kod bazalaridagi kutilmagan nil xatolarining eng keng tarqalgan manbai bo'lib, u xatolarni qayta ishlash kodida doimiy paydo bo'ladi:
func doSomething() *MyError {
return nil // xato yuz bermadi
}
func run() error {
var err *MyError = doSomething()
return err // XATO: nil bo'lmagan error interface qaytaradi!
}
func caller() {
if err := run(); err != nil {
fmt.Println("got an error:", err) // hech narsa noto'g'ri ketmagan bo'lsa ham, bu tarmoq ishlaydi
}
}
run error (interface) qaytarish tipini e'lon qiladi, lekin unga aniq *MyErrorni o'zlashtiradi. Bu pointer nil bo'lsa ham, uni error interface'iga o'rash nil bo'lmagan interface qiymatini hosil qiladi, shuning uchun chaqiruv joyida err != nil true bo'ladi, va muvaffaqiyatda xatoni qayta ishlash tarmog'ini o'tkazib yuborish uchun yozilgan kod uni baribir ishga tushiradi. Tuzatish deyarli har doim aniq nil-likning interface'ga sizib kirishiga yo'l qo'ymaslik — xato bo'lmaganda interface tipini to'g'ridan-to'g'ri qaytaring:
func run() error {
if err := doSomething(); err != nil {
return err
}
return nil // haqiqiy nil interface, ikkala yarmi bo'sh
}
Xuddi shu tuzoq funksiya interface parametrini qabul qilganda va kimdir aniq tipdagi nil pointer'ni uzatganda ham paydo bo'ladi:
func process(w Writer) {
if w == nil {
fmt.Println("nil writer")
return
}
w.Write([]byte("data")) // hamon panic qilishi mumkin!
}
var f *File
process(f) // w nil emas (tipi *File, qiymati nil) — yuqoridagi nil tekshiruvi buni ushlamaydi
Agar bundan himoyalanish kerak bo'lsa, aniq pointer'ni interface'ga qutilanishidan oldin tekshiring, yoki — odatda yaxshiroq tuzatish — metodning o'zini nil receiver'ga nisbatan himoyalovchi qiling, chunki Go metod tanasi uni xavfli dereferens qilmasa, nil pointer orqali metod chaqirishga ruxsat beradi:
func (f *File) Write(data []byte) (int, error) {
if f == nil {
return 0, errors.New("write on nil *File")
}
// ...
return len(data), nil
}
nil bo'lmagan interface ichida yashiringan nil aniq qiymatni aniqlash kerak bo'lganda, tip tasdiqlash to'g'ridan-to'g'ri vosita:
if buf, ok := w.(*bytes.Buffer); ok && buf == nil {
// interface nil emas, lekin u ushlab turgan *bytes.Buffer nil
}
nil pointer'ni interface'ga o'zlashtirish nil bo'lmagan interface hosil qiladi.
Tip interface'ni qanoatlantiradimi-yo'qmi, faqat metod nomlariga emas, metodlar value yoki pointer receiver bilan e'lon qilinganiga ham bog'liq, va bu Goning method set'lari bilan qiziqarli tarzda o'zaro ta'sir qiladi:
type Writer interface {
Write([]byte) (int, error)
}
type File struct{}
func (f File) Write(data []byte) (int, error) { return 0, nil }
var w1 Writer = File{} // OK — value receiver `File` ham, `*File` ham Writer'ni qanoatlantiradi degani
var w2 Writer = &File{} // bu ham OK
func (f *File) Write(data []byte) (int, error) { return 0, nil }
var w1 Writer = File{} // kompilyatsiya xatosi — File Writer'ni qanoatlantirmaydi
var w2 Writer = &File{} // OK — Write faqat *File ning method set'ida
Qoida: value receiver metod ham T, ham *T method set'lariga tegishli, lekin pointer receiver metod faqat *T method set'iga tegishli. Sabab addressability bilan bog'liq, o'zboshimchalik bilan qo'yilgan cheklov emas — pointer-receiver metodni chaqirish o'zgartirish uchun pointer talab qiladi, va Go addressable o'zgaruvchining manzilini sizning o'rningizga mamnuniyat bilan oladi (lokal Filedagi f.Write(...) (&f).Write(...)ga aylanadi), ammo oddiy interface qiymati bu ma'noda addressable emas, shuning uchun kompilyator oddiy Filening *File metodlarini talab qiladigan interface'ni qanoatlantirishiga shunchaki yo'l qo'ymaydi. Amalda bu shuni anglatadi: agar tipdagi biror metod pointer receiver ishlatsa (odatda u receiver'ni o'zgartirishi kerakligi yoki struct katta va uni nusxalash isrofgarchilik bo'lgani uchun), shu tipni interface sifatida uzatadigan hamma joyda pointer ishlating, va bitta tipning metodlarida value va pointer receiver'larni aralashtirish o'rniga receiver turida izchil bo'ling.
Bularning hammasi nega muhimligining eng aniq namoyishi — bir necha aloqasiz tip bir-birining o'rniga ishlatilishi kerak bo'lgan holat. Kredit karta, PayPal yoki bank o'tkazmasi bilan to'lov olishda bir xil ishlashi kerak bo'lgan to'lov xizmatini ko'rib chiqing:
package main
import "fmt"
// PaymentProcessor — PaymentService o'z to'lov usuli haqida biladigan yagona narsa —
// uchta tor metod, ularning hech birining aslida qanday amalga oshirilgani haqida hech narsa yo'q.
type PaymentProcessor interface {
ProcessPayment(amount float64) error
Refund(transactionID string) error
GetBalance() float64
}
type CreditCardProcessor struct {
CardNumber string
Balance float64
}
func (c *CreditCardProcessor) ProcessPayment(amount float64) error {
if amount <= 0 {
return fmt.Errorf("invalid amount: %.2f", amount)
}
if c.Balance < amount {
return fmt.Errorf("insufficient balance")
}
c.Balance -= amount
return nil
}
func (c *CreditCardProcessor) Refund(transactionID string) error {
fmt.Printf("refunding transaction %s to card %s\n", transactionID, c.CardNumber)
return nil
}
func (c *CreditCardProcessor) GetBalance() float64 { return c.Balance }
type PayPalProcessor struct {
Email string
Balance float64
}
func (p *PayPalProcessor) ProcessPayment(amount float64) error {
if amount <= 0 {
return fmt.Errorf("invalid amount: %.2f", amount)
}
if p.Balance < amount {
return fmt.Errorf("insufficient balance")
}
p.Balance -= amount
return nil
}
func (p *PayPalProcessor) Refund(transactionID string) error {
fmt.Printf("refunding transaction %s via PayPal (%s)\n", transactionID, p.Email)
return nil
}
func (p *PayPalProcessor) GetBalance() float64 { return p.Balance }
// PaymentService kredit kartalar yoki PayPal haqida hech narsa bilmaydi — faqat PaymentProcessor'ni.
type PaymentService struct {
processor PaymentProcessor
}
func NewPaymentService(processor PaymentProcessor) *PaymentService {
return &PaymentService{processor: processor}
}
func (ps *PaymentService) Charge(amount float64) error {
return ps.processor.ProcessPayment(amount)
}
func main() {
cc := &CreditCardProcessor{CardNumber: "1234-5678", Balance: 1000.0}
pp := &PayPalProcessor{Email: "user@example.com", Balance: 500.0}
for _, processor := range []PaymentProcessor{cc, pp} {
service := NewPaymentService(processor)
if err := service.Charge(100.0); err != nil {
fmt.Println("charge failed:", err)
continue
}
fmt.Printf("remaining balance: $%.2f\n", processor.GetBalance())
}
}
PaymentService interface'ga qarshi bir marta yozilgan va ikkala processor bilan ishlaydi — va har qanday kelajakdagi, masalan bank o'tkazmasi yoki test double bilan, PaymentServicening bitta qatori o'zgarmasdan. Bu oxirgi nuqta polimorfizmning o'zidek muhim: PaymentProcessorda faqat uchta metod bo'lgani uchun, PaymentService uchun unit test bir necha qatorda oddiy xotiradagi soxtani amalga oshirishi mumkin, haqiqiy to'lov shlyuzini ko'tarish yoki mocking frameworkiga murojaat qilish o'rniga.
Standart kutubxona qanday qurilganidan to'g'ridan-to'g'ri olingan bir necha odat yaxshi eskiradigan interface'larni beradi:
Ularni kichik saqlang. Bir dan uch metodgacha — eng yaxshi nuqta; beshtadan oshgach, interface chindan bitta imkoniyatni tasvirlaydimi yoki bir nechtasi birga bog'langanmi, deb so'rang. Kichik interface'larni amalga oshirish arzon, testlarda soxtalashtirish arzon va bir-biri bilan erkin birikadi.
Interface'larni amalga oshirilgan joyda emas, iste'mol qilingan joyda aniqlang. Bir necha baytni o'qishi kerak bo'lgan funksiya *os.Fileni talab qilmasdan, func process(r io.Reader) error deb e'lon qilishi kerak — garchi bugun yagona chaqiruvchi tasodifan fayl uzatsa ham. Aniq tip ishlab chiqaruvchisi uning chaqiruvchilari pirovard natijada xohlashi mumkin bo'lgan har bir interface'ni oldindan bashorat qilishi shart emas; har bir chaqiruvchiga talabni aynan o'zi kerak bo'lgan narsagacha qisqartirishga imkon bering. Goda, Javadan farqli o'laroq, paket o'z bog'liqliklaridan nima kerakligini tasvirlash uchungina kichik interface aniqlashi, hatto boshqa joyda mutlaqo yaxshi aniq tip mavjud bo'lsa ham, mutlaqo normal va kutilgan hol.
Hamma narsani qiladigan bitta interface o'rniga interface segregation'ni afzal ko'ring. Agar ba'zi chaqiruvchilarga faqat yurish, boshqalariga faqat suzish kerak bo'lsa, har bir amalga oshiruvchini ikkalasini qilishga majburlamang:
type Walker interface{ Walk() }
type Swimmer interface{ Swim() }
// tip faqat aslida qila oladigan narsasini amalga oshiradi
type Dog struct{}
func (d Dog) Walk() {}
type Fish struct{}
func (f Fish) Swim() {}
Interface qabul qiling, aniq tip qaytaring. Bu tamoyil, keyingi bobda chuqur muhokama qilinadi, aslida "interface'larni iste'mol qilingan joyda aniqlang"ning alohida holati: funksiya parametrlari aynan interface'ga toraytirish o'zini oqlaydigan joy, qaytariladigan qiymat esa odatda aniq qoldirilgani yaxshi, shunda chaqiruvchilar ishlash uchun to'liq method set'ni oladi va kompilyator ularga nimani ushlab turishlarini aniq aytadi.
Interface'lar — Goning ajratishning (decoupling) asosiy vositasi — ikkita paketning hech biri boshqasining ichki qismlariga bog'liq bo'lmasdan shakl bo'yicha kelishib olishi uchun. Yaxshi ishlatilganda, kichik dozalarda, paketlar orasidagi chegaralarda, ular kodni testlashni osonlashtiradi va keyinroq o'zgartirishni osonlashtiradi. Hamma joyda, har bir struct'da, "har ehtimolga qarshi" ishlatilganda, ular asosan hech kim so'ramagan bilvositalik qatlamini qo'shadi. Keyingi bob shu murosani to'g'ridan-to'g'ri ko'rib chiqadi, aniq qoidalar — interface qabul qiling, ularni kichik saqlang, ularni ehtiyotsizlik bilan versiyalamang — o'z og'irligini ko'taradigan interface'larni ko'tarmaydiganlaridan ajratadi.
Godagi interface — metod signaturalari to'plami; shu metodlarga ega har qanday tip uni avtomatik qanoatlantiradi.
Mumkin bo'lgan eng oddiy interface'dan boshlang:
type Writer interface {
Write([]byte) (int, error)
}
Bu Writer nima ekani haqida hech narsa demaydi — faqat u nima qila olishini aytadi. Istalgan paketdagi, istalgan kishi tomonidan, istalgan vaqtda yozilgan har qanday tip aynan shu signaturali metodga ega bo'lgan zahoti Writerni qanoatlantiradi:
type File struct {
// ...
}
func (f *File) Write(data []byte) (int, error) {
// ma'lumotni asosiy fayl deskriptoriga yozish
return len(data), nil
}
var w Writer = &File{}
Filening e'lonida Writer tilga olinmaydi. Kompilyator munosabatni strukturaviy tarzda, method set'larni solishtirish orqali, *Fileni Writer sifatida ishlatmoqchi bo'lgan nuqtada topadi. Buni odatda strukturaviy tiplashtirish (structural typing) deyishadi (Java yoki C#'ning nominal tiplashtirishidan farqli, u yerda klass Writer sifatida qaralishi uchun aniq implements Writer deb e'lon qilishi kerak), va ba'zan eski Python iborasi "duck typing" bilan ta'riflanadi — agar u o'rdakdek yursa va o'rdakdek qaqillasa, uni o'rdak deb hisoblang. Go kompilyatorining versiyasi Pythonnikidan qattiqroq, chunki u hamon metod signaturalarini kompilyatsiya paytida tekshiradi; faqat munosabatni oldindan e'lon qilishingiz shart emas.
Amaliy oqibat ulkan: siz o'z paketingizda aynan sizga kerak bo'lgan xatti-harakatni tasvirlaydigan interface aniqlashingiz mumkin, va har qanday tip — standart kutubxonadagi yoki siz nazorat qilmaydigan uchinchi tomon modulidagi tiplar ham — to'g'ri metodlarga ega bo'lsa, uni avtomatik qanoatlantiradi. os.File muallifidan uni o'zingizning tor Writer interface'ingiz sifatida ko'rish uchun ruxsat kerak emas. Odamlar Go interface'lari ishlab chiqaruvchi tomonidan emas, iste'molchi tomonidan aniqlanadi deganda shuni nazarda tutadi: qiymatni ishlatadigan kod unga qanday shakl kerakligini hal qiladi, tip muallifi u qanoatlantirishi so'ralishi mumkin bo'lgan har bir interface'ni oldindan bashorat qilishi shart emas.
Bitta aniq tip, va odatda shunday, bir vaqtning o'zida ko'plab interface'larni qanoatlantiradi, chunki interface'lar erkin ustma-ust tushadi:
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}
func (p Person) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Name string `json:"name"`
Age int `json:"age"`
}{p.Name, p.Age})
}
Person String() string metodiga ega bo'lgani uchun fmt.Stringerni qanoatlantiradi, va alohida MarshalJSON() ([]byte, error) metodiga ega bo'lgani uchun json.Marshalerni qanoatlantiradi. Ikkala interface bir-biri haqida bilmaydi, va Personga ularni amalga oshirishi aytilishi shart emas — fmt.Println(p) va json.Marshal(p) har biri ichkarida tip tasdiqlash (type assertion) orqali tegishli metodni o'zi topadi.
Buni tip faqat o'z method set'iga qarab mos keladigan yoki kelmaydigan ko'plab kichik interface "shakllari" sifatida tasavvur qilish mumkin:
*File
┌───────────────┐
│ Read() │───────┐
│ Write() │───┐ │
│ Close() │ │ │
└───────────────┘ │ │
│ │
Writer │ │
┌───────────────┐ │ │
│ Write() │◄───┘ │
└───────────────┘ │
│
ReadWriter │
┌───────────────┐ │
│ Read() │◄────────┘
│ Write() │◄─────── *File buni ham qanoatlantiradi
└───────────────┘
*Buffer, *Conn va mos Write metodiga ega har qanday boshqa tip
ham Writer'ni qanoatlantiradi — ro'yxat yo'q, ulashilgan
umumiy tip yo'q.
Standart kutubxonaning eng ta'sirli ikkita interface'i ham bitta metodli:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Goda baytlarni ko'chiradigan deyarli hamma narsa — fayllar, tarmoq ulanishlari, xotiradagi buferlar, kompressorlar, xesh funksiyalari, TLS oqimlari — bittasini yoki ikkalasini qanoatlantiradi. Bu standart kutubxona tarixining tasodifi emas; bu interface'larni kichik saqlashning to'g'ridan-to'g'ri foydasi. Bir metodli interface'ni amalga oshirish deyarli bepul (to'g'ri metodga ega har qanday tip, muallifi io.Reader haqida hech eshitmagan bo'lsa ham, uni allaqachon qanoatlantiradi), va uni testda soxtalashtirish arzimas. O'n ikki metodli interface esa har bir amalga oshiruvchini o'ziga kerak bo'lmasligi mumkin bo'lgan o'n bitta metod yozishga majburlaydi, va har bir test double'ni hech qachon chaqirmaydigan o'n bitta metodni stub qilishga majburlaydi.
Bitta metoddan ko'proq xatti-harakat kerak bo'lganda, Goning javobi kattaroq interface emas, kompozitsiya:
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
io.Closer
}
Bir interface'ni boshqasi ichiga embedding qilish ularning method set'larini shunchaki birlashtiradi. ReadWriteCloserni Read, Write va Close metodlariga ega har qanday tip qanoatlantiradi — bu metodlar turlicha nomlangan interface'lar orasida tarqalganmi yoki yo'qmi ahamiyatsiz; kompilyator faqat hosil bo'lgan yassi method set'ga qaraydi. Aynan shu sababli standart kutubxonada har bir funksiya aynan kerakli shaklga birikadigan kichik interface'lar alifbosi (Reader, Writer, Closer, Seeker, ReaderAt, …) bor, hamma narsa to'liq amalga oshirishi kerak bo'lgan yagona monolit Filega o'xshash interface o'rniga.
anyGodagi har bir tip nol metodli interface'ni qanoatlantiradi:
var x interface{} = 42
var y interface{} = "hello"
var z interface{} = []int{1, 2, 3}
Go 1.18 dan beri any — interface{} uchun oldindan e'lon qilingan taxallus, va bugungi idiomatik kod bo'sh interface kerak bo'lgan deyarli hamma joyda any yozadi — interface{} hamon ishlaydi va uni 1.18 dan oldin yozilgan kodda ko'rasiz, lekin yangi kodda unga ustunlik berishga sabab yo'q. Ikkala yozuv kompilyator uchun bir xil ma'noni beradi: hech qanday kompilyatsiya vaqtidagi xatti-harakat kafolatini olib yurmaydigan qiymat. any — fmt.Println argumentlarining tipi, ixtiyoriy JSON'ni dekodlayotganda map[string]anyning tipi va shakli noma'lum qiymatlarni chindan qabul qilishi kerak bo'lgan har qanday boshqa narsaning tipi.
anyning narxi shuki, u statik tip tekshiruvini tashlab yuboradi. Qiymat any sifatida qutiga solingach, kompilyator u bilan nima qilayotganingizni endi tekshira olmaydi; har bir amal ish vaqtida tekshiriladigan tip tasdiqlash yoki type switch orqali o'tishi kerak, va har bir xato kompilyatsiya xatosi o'rniga panic'ga aylanadi. Generics Go 1.18 da paydo bo'lishidan oldin any (yoki interface{}) shuningdek bir nechta tip bilan ishlaydigan funksiya yozishning yagona yo'li edi — sort.Interface, eski container/list va son-sanoqsiz qo'lda yozilgan "generic" konteynerlar hammasi unga tayanardi. Bugun, agar sizga aslida "bir nechta aniq tip ustida ishlaydigan, to'liq kompilyatsiya vaqtidagi tip xavfsizligi bilan bir xil mantiq" kerak bo'lsa, type parameter deyarli har doim yaxshiroq vosita; anyga tipi ish vaqtigacha noma'lum qiymatlarni chindan qayta ishlash kerak bo'lganda — tipsiz JSON'ni dekodlash, fmt.Stringer uslubidagi formatlashni amalga oshirish yoki generics'dan oldingi kodda generic konteynerni eski usulda qurish — murojaat qiling.
Bu Go interface dizaynining yangi boshlovchilarni eng ko'p qoqintiradigan qismi, va uni qoidani yodlash o'rniga aniq aqliy model qurish foydali. Interface qiymati — juftlik: tip va shu tipdagi qiymat. Yozganingizda
var w Writer
ikkala yarmi ham bo'sh interface qiymatini olasiz — tip yo'q, qiymat yo'q. w == nil aynan shuni (va faqat shuni) tekshiradi. Aniq qiymatni interface o'zgaruvchisiga o'zlashtirgan zahoti, aniq qiymatning o'zi nil bo'lsa ham, ikkala yarmi to'ladi:
var f *File = nil
var w Writer = f
// w nil EMAS — u tipga (*File) va qiymatga (nil *File pointer) ega
w nil interface emas. U (*File, nil) juftligini ushlab turgan interface. Bu e'tibordan chetda qoldirsa bo'ladigan chekka holat emas; bu Go kod bazalaridagi kutilmagan nil xatolarining eng keng tarqalgan manbai, va u xatolarni qayta ishlash kodida doimiy paydo bo'ladi:
func doSomething() *MyError {
return nil // xato yuz bermadi
}
func run() error {
var err *MyError = doSomething()
return err // XATO: nil bo'lmagan error interface qaytaradi!
}
func caller() {
if err := run(); err != nil {
fmt.Println("got an error:", err) // hech narsa noto'g'ri ketmagan bo'lsa ham, bu tarmoq ishlaydi
}
}
run error (interface) qaytarish tipini e'lon qiladi, lekin unga aniq *MyErrorni o'zlashtiradi. Bu pointer nil bo'lsa ham, uni error interface'iga o'rash nil bo'lmagan interface qiymatini hosil qiladi, shuning uchun chaqiruv joyida err != nil true bo'ladi, va muvaffaqiyatda xatoni qayta ishlash tarmog'ini o'tkazib yuborish uchun yozilgan kod uni baribir ishga tushiradi. Tuzatish deyarli har doim aniq nil-likning interface'ga sizib kirishiga yo'l qo'ymaslik — xato bo'lmaganda interface tipini to'g'ridan-to'g'ri qaytaring:
func run() error {
if err := doSomething(); err != nil {
return err
}
return nil // haqiqiy nil interface, ikkala yarmi bo'sh
}
Xuddi shu tuzoq funksiya interface parametrini qabul qilganda va kimdir aniq tipdagi nil pointer'ni uzatganda paydo bo'ladi:
func process(w Writer) {
if w == nil {
fmt.Println("nil writer")
return
}
w.Write([]byte("data")) // hamon panic qilishi mumkin!
}
var f *File
process(f) // w nil emas (tipi *File, qiymati nil) — yuqoridagi nil tekshiruvi buni ushlamaydi
Agar bundan himoyalanish kerak bo'lsa, aniq pointer'ni interface'ga qutilanishidan oldin tekshiring, yoki — odatda yaxshiroq tuzatish — metodning o'zini nil receiver'ga nisbatan himoyalovchi qiling, chunki Go metod tanasi uni xavfli dereferens qilmasa, nil pointer orqali metod chaqirishga ruxsat beradi:
func (f *File) Write(data []byte) (int, error) {
if f == nil {
return 0, errors.New("write on nil *File")
}
// ...
return len(data), nil
}
nil bo'lmagan interface ichida yashiringan nil aniq qiymatni aniqlash kerak bo'lganda, tip tasdiqlash to'g'ridan-to'g'ri vosita:
if buf, ok := w.(*bytes.Buffer); ok && buf == nil {
// interface nil emas, lekin u ushlab turgan *bytes.Buffer nil
}
nil pointer'ni interface'ga o'zlashtirish nil bo'lmagan interface hosil qiladi.
Tip interface'ni qanoatlantiradimi-yo'qmi, faqat metod nomlariga emas, metodlar value yoki pointer receiver bilan e'lon qilinganiga ham bog'liq, va bu Goning method set'lari bilan qiziqarli tarzda o'zaro ta'sir qiladi:
type Writer interface {
Write([]byte) (int, error)
}
type File struct{}
func (f File) Write(data []byte) (int, error) { return 0, nil }
var w1 Writer = File{} // OK — value receiver `File` ham, `*File` ham Writer'ni qanoatlantiradi degani
var w2 Writer = &File{} // bu ham OK
func (f *File) Write(data []byte) (int, error) { return 0, nil }
var w1 Writer = File{} // kompilyatsiya xatosi — File Writer'ni qanoatlantirmaydi
var w2 Writer = &File{} // OK — Write faqat *File ning method set'ida
Qoida: value receiver metod ham T, ham *T method set'lariga tegishli, lekin pointer receiver metod faqat *T method set'iga tegishli. Sabab addressability bilan bog'liq, o'zboshimchalik bilan cheklov emas — pointer-receiver metodni chaqirish o'zgartirish uchun pointer kerak qiladi, va Go addressable o'zgaruvchining manzilini sizning o'rningizga mamnuniyat bilan oladi (lokal Filedagi f.Write(...) (&f).Write(...)ga aylanadi), oddiy interface qiymati bu ma'noda addressable emas, shuning uchun kompilyator oddiy Filening *File metodlarini talab qiladigan interface'ni qanoatlantirishiga shunchaki yo'l qo'ymaydi. Amalda bu shuni anglatadi: agar tipdagi biror metod pointer receiver ishlatsa (odatda u receiver'ni o'zgartirishi kerakligi yoki struct katta va uni nusxalash isrofgarchilik bo'lgani uchun), shu tipni interface sifatida uzatadigan hamma joyda pointer ishlating, va bitta tipning metodlarida value va pointer receiver'larni aralashtirish o'rniga receiver turida izchil bo'ling.
Bularning hammasi nega muhimligining eng aniq namoyishi — bir necha aloqasiz tip bir-birining o'rniga ishlatilishi kerak bo'lgan holat. Kredit karta, PayPal yoki bank o'tkazmasi bilan to'lov olishda bir xil ishlashi kerak bo'lgan to'lov xizmatini ko'rib chiqing:
package main
import "fmt"
// PaymentProcessor — PaymentService o'z to'lov usuli haqida biladigan yagona narsa —
// uchta tor metod, ularning hech birining aslida qanday amalga oshirilgani haqida hech narsa yo'q.
type PaymentProcessor interface {
ProcessPayment(amount float64) error
Refund(transactionID string) error
GetBalance() float64
}
type CreditCardProcessor struct {
CardNumber string
Balance float64
}
func (c *CreditCardProcessor) ProcessPayment(amount float64) error {
if amount <= 0 {
return fmt.Errorf("invalid amount: %.2f", amount)
}
if c.Balance < amount {
return fmt.Errorf("insufficient balance")
}
c.Balance -= amount
return nil
}
func (c *CreditCardProcessor) Refund(transactionID string) error {
fmt.Printf("refunding transaction %s to card %s\n", transactionID, c.CardNumber)
return nil
}
func (c *CreditCardProcessor) GetBalance() float64 { return c.Balance }
type PayPalProcessor struct {
Email string
Balance float64
}
func (p *PayPalProcessor) ProcessPayment(amount float64) error {
if amount <= 0 {
return fmt.Errorf("invalid amount: %.2f", amount)
}
if p.Balance < amount {
return fmt.Errorf("insufficient balance")
}
p.Balance -= amount
return nil
}
func (p *PayPalProcessor) Refund(transactionID string) error {
fmt.Printf("refunding transaction %s via PayPal (%s)\n", transactionID, p.Email)
return nil
}
func (p *PayPalProcessor) GetBalance() float64 { return p.Balance }
// PaymentService kredit kartalar yoki PayPal haqida hech narsa bilmaydi — faqat PaymentProcessor'ni.
type PaymentService struct {
processor PaymentProcessor
}
func NewPaymentService(processor PaymentProcessor) *PaymentService {
return &PaymentService{processor: processor}
}
func (ps *PaymentService) Charge(amount float64) error {
return ps.processor.ProcessPayment(amount)
}
func main() {
cc := &CreditCardProcessor{CardNumber: "1234-5678", Balance: 1000.0}
pp := &PayPalProcessor{Email: "user@example.com", Balance: 500.0}
for _, processor := range []PaymentProcessor{cc, pp} {
service := NewPaymentService(processor)
if err := service.Charge(100.0); err != nil {
fmt.Println("charge failed:", err)
continue
}
fmt.Printf("remaining balance: $%.2f\n", processor.GetBalance())
}
}
PaymentService interface'ga qarshi bir marta yozilgan va ikkala processor bilan ishlaydi — va har qanday kelajakdagi, masalan bank o'tkazmasi yoki test double bilan, PaymentServicening bitta qatori o'zgarmasdan. Bu oxirgi nuqta polimorfizmning o'zidek muhim: PaymentProcessorda faqat uchta metod bo'lgani uchun, PaymentService uchun unit test bir necha qatorda oddiy xotiradagi soxtani amalga oshirishi mumkin, haqiqiy to'lov shlyuzini ko'tarish yoki mocking frameworkiga murojaat qilish o'rniga.
Standart kutubxona qanday qurilganidan to'g'ridan-to'g'ri olingan bir necha odat yaxshi eskiradigan interface'larni beradi:
Ularni kichik saqlang. Bir dan uch metodgacha — eng yaxshi nuqta; beshtadan oshgach, interface chindan bitta imkoniyatni tasvirlaydimi yoki bir nechtasi birga bog'langanmi, deb so'rang. Kichik interface'larni amalga oshirish arzon, testlarda soxtalashtirish arzon va bir-biri bilan erkin birikadi.
Interface'larni amalga oshirilgan joyda emas, iste'mol qilingan joyda aniqlang. Bir necha baytni o'qishi kerak bo'lgan funksiya *os.Fileni talab qilmasdan, func process(r io.Reader) error deb e'lon qilishi kerak — garchi bugun yagona chaqiruvchi tasodifan fayl uzatsa ham. Aniq tip ishlab chiqaruvchisi uning chaqiruvchilari oxir-oqibat xohlashi mumkin bo'lgan har bir interface'ni oldindan bashorat qilishi shart emas; har bir chaqiruvchiga talabni aynan o'zi kerak bo'lgan narsagacha qisqartirishga imkon bering. Goda, Javadan farqli o'laroq, paket o'z bog'liqliklaridan nima kerakligini tasvirlash uchungina kichik interface aniqlashi, hatto boshqa joyda mutlaqo yaxshi aniq tip mavjud bo'lsa ham, mutlaqo normal va kutilgan hol.
Hamma narsani qiladigan bitta interface o'rniga interface segregation'ni afzal ko'ring. Agar ba'zi chaqiruvchilarga faqat yurish, boshqalariga faqat suzish kerak bo'lsa, har bir amalga oshiruvchini ikkalasini qilishga majburlamang:
type Walker interface{ Walk() }
type Swimmer interface{ Swim() }
// tip faqat aslida qila oladigan narsasini amalga oshiradi
type Dog struct{}
func (d Dog) Walk() {}
type Fish struct{}
func (f Fish) Swim() {}
Interface qabul qiling, aniq tip qaytaring. Bu tamoyil, keyingi bobda chuqur muhokama qilinadi, aslida "interface'larni iste'mol qilingan joyda aniqlang"ning alohida holati: funksiya parametrlari aynan interface'ga toraytirish o'zini oqlaydigan joy, qaytariladigan qiymat esa odatda aniq qoldirilgani yaxshi, shunda chaqiruvchilar ishlash uchun to'liq method set'ni oladi va kompilyator ularga nimani ushlab turishlarini aniq aytadi.
Interface'lar — Goning ajratishning (decoupling) asosiy vositasi — ikkita paketning hech biri boshqasining ichki qismlariga bog'liq bo'lmasdan shakl bo'yicha kelishib olishi uchun. Yaxshi ishlatilganda, kichik dozalarda, paketlar orasidagi chegaralarda, ular kodni testlashni osonlashtiradi va keyinroq o'zgartirishni osonlashtiradi. Hamma joyda, har bir struct'da, "har ehtimolga qarshi" ishlatilganda, ular asosan hech kim so'ramagan bilvositalik qatlamini qo'shadi. Keyingi bob shu murosani to'g'ridan-to'g'ri ko'rib chiqadi, aniq qoidalar — interface qabul qiling, ularni kichik saqlang, ularni ehtiyotsizlik bilan versiyalamang — o'z og'irligini ko'taradigan interface'larni ko'tarmaydiganlaridan ajratadi.