Інтерфейси¶
Інтерфейс — це тип, який перелічує набір сигнатур методів. Будь-яке значення, тип якого має всі ці методи, задовольняє інтерфейс — і його можна зберегти у змінній цього інтерфейсного типу. Інтерфейси — це те, як Go реалізує поліморфізм: код залежить від того, що значення вміє робити (його методів), а не від його конкретного типу.
Shape тепер є типом. Змінна типу Shape може містити будь-яке значення,
що має метод Area() float64.
Задоволення є неявним¶
Ключового слова implements немає. Тип задовольняє інтерфейс просто тим,
що має потрібні методи — компілятор перевіряє це структурно. Ви ніколи не
оголошуєте цей зв'язок; він просто існує.
type Rectangle struct{ W, H float64 }
func (r Rectangle) Area() float64 { return r.W * r.H }
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
var s Shape = Rectangle{W: 3, H: 4} // Rectangle задовольняє Shape — оголошення не потрібне
fmt.Println(s.Area()) // output: 12
І Rectangle, і Circle задовольняють Shape, жодного разу його не
згадавши.
З погляду Python: це качина типізація — «якщо має методи, то підходить» — але перевірена на етапі компіляції. Тип, у якого бракує методу, просто не скомпілюється там, де очікується інтерфейс, замість того щоб впасти під час виконання.
Поліморфізм: одна функція, багато типів¶
Оскільки будь-який Shape має Area(), функція може приймати інтерфейс і
працювати з кожним конкретним типом однаково:
func totalArea(shapes []Shape) float64 {
sum := 0.0
for _, s := range shapes {
sum += s.Area()
}
return sum
}
shapes := []Shape{Rectangle{3, 4}, Circle{1}}
fmt.Printf("%.2f\n", totalArea(shapes)) // output: 15.14
Інтерфейсне значення — це пара (тип, значення)¶
Усередині інтерфейсне значення містить дві речі: динамічний тип того,
що збережено, і саме значення. Нульове значення інтерфейсу — nil:
ні типу, ні значення.
Виклик методу на nil-інтерфейсі спричиняє паніку, бо немає конкретного
методу, до якого можна було б диспетчеризувати виклик:
Щойно інтерфейс тримає значення, він пам'ятає динамічний тип цього
значення — його видно через %T:
Отримувачі за вказівником чи за значенням визначають задоволення¶
Це найпоширеніша пастка інтерфейсів. Набір методів типу визначає, які інтерфейси він задовольняє:
- методи з отримувачем за значенням належать і
T, і*T - методи з отримувачем за вказівником належать лише
*T
Тож якщо метод має отримувача за вказівником, інтерфейс задовольняє лише вказівник — значення ні:
type Counter struct{ n int }
func (c *Counter) Add() { c.n++ } // отримувач за вказівником
func (c Counter) Value() int { return c.n }
type Adder interface{ Add() }
var a Adder = &Counter{} // ок: *Counter має Add
// var a Adder = Counter{} // compile error: Counter does not implement Adder
// // (method Add has pointer receiver)
a.Add()
Емпіричне правило: якщо хоч одному методу потрібен отримувач за вказівником, передавайте вказівник, коли хочете, щоб значення задовольняло інтерфейс.
Порожній інтерфейс та any¶
Інтерфейс без методів задовольняє кожен тип. Його сучасне написання —
any (псевдонім для interface{}), і це спосіб тримати «значення
невідомого типу».
any — це засіб останньої надії: ви втрачаєте всю інформацію про тип на
етапі компіляції. Щоб дістати конкретне значення назад, ви використовуєте
твердження типу або перемикач типів — це розглядається в
наступній статті.
З погляду Python:
any— це найближче до звичайного посилання наobject: воно може тримати будь-що, і перед конкретним використанням треба перевірити тип.
Пастка типізованого nil¶
Інтерфейс є nil, лише коли і його тип, і значення є nil. Якщо ви
збережете nil-вказівник в інтерфейсі, інтерфейс тримає тип, тож він
не nil — часте джерело багів.
type T struct{}
func (t *T) Foo() {}
var p *T = nil
var i interface{ Foo() } = p
fmt.Println(p == nil) // output: true
fmt.Println(i == nil) // output: false — i має тип *T, тож він не-nil
Висновок: повертайте буквальний nil для інтерфейсу, а не nil конкретний
вказівник, коли маєте на увазі «нічого».
Маленькі інтерфейси — це ідіоматично¶
Go надає перевагу крихітним інтерфейсам — часто з одним методом — які оголошують там, де їх використовують, а не там, де визначають типи. Стандартна бібліотека ними рясніє:
| Інтерфейс | Метод | Призначення |
|---|---|---|
fmt.Stringer |
String() string |
власна текстова форма |
error |
Error() string |
тип помилки |
io.Reader |
Read([]byte) (int, error) |
джерело байтів |
io.Writer |
Write([]byte) (int, error) |
приймач байтів |
sort.Interface |
Len/Less/Swap |
власне сортування |
Реалізуйте fmt.Stringer, і fmt.Println використає його автоматично:
type Color struct{ R, G, B int }
func (c Color) String() string {
return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
}
fmt.Println(Color{255, 165, 0}) // output: #FFA500
Настанова: приймайте інтерфейси, повертайте конкретні типи. Беріть найменший інтерфейс, який ваша функція справді потребує, як параметр, а повертайте конкретну структуру, щоб виклику лишалася повна інформація.
Швидка довідка¶
| Поняття | Синтаксис |
|---|---|
| оголосити інтерфейс | type Reader interface { Read(p []byte) (int, error) } |
| задовольнити його | просто визначте методи — без ключового слова |
| порожній інтерфейс | any (= interface{}), тримає будь-яке значення |
| nil-інтерфейс | і тип, і значення є nil |
Витягання конкретного значення назад з інтерфейсу розглядається далі — у твердженнях типу та перемикачах типів.