Урок 6 из 8 · 25 мин · обновлено
Структуры, методы и интерфейсы
Как описывать свои типы данных через struct, добавлять им методы, почему метод с указателем меняет объект и как интерфейсы позволяют работать с разными типами одинаково.
Структуры
До сих пор каждое значение хранилось в отдельной переменной. Но заказ — это сразу клиент, список товаров и сумма. Чтобы собрать такие данные в один тип, используют структуру:
type Item struct {
Title string
Price int
Qty int
}
type Order struct {
Customer string
Items []Item
}
type Item struct { … } создаёт новый тип с полями. Поля бывают любого типа, в том числе срезом других структур, как Items.
Значение структуры создают, перечисляя поля, а к полям обращаются через точку:
it := Item{Title: "Чай", Price: 120, Qty: 3}
fmt.Println(it.Title, it.Price)
fmt.Printf("%+v\n", it)
Чай 120
{Title:Чай Price:120 Qty:3}
Поля, которые не указали, получают нулевые значения. Шаблон %+v выводит структуру вместе с названиями полей — удобно при отладке.
Методы
Метод — функция, привязанная к типу. Перед её именем в скобках пишут получатель: значение, у которого метод вызывают.
func (o Order) Total() int {
sum := 0
for _, it := range o.Items {
sum += it.Price * it.Qty
}
return sum
}
func (o *Order) Add(it Item) {
o.Items = append(o.Items, it)
}
o := Order{Customer: "Анна"}
o.Add(Item{Title: "Пицца", Price: 650, Qty: 1})
o.Add(Item{Title: "Морс", Price: 150, Qty: 2})
fmt.Println(o.Customer, len(o.Items), o.Total())
Анна 2 950
Обратите внимание на звёздочку в (o *Order). Это указатель: метод работает с самим заказом, а не с его копией. Без звёздочки метод получил бы копию: добавил бы товар в неё, и после вызова в заказе ничего бы не изменилось. Правило простое:
- метод только читает данные — получатель без звёздочки, как у
Total; - метод меняет данные — получатель с
*, как уAdd.
Интерфейсы
Допустим, уведомления о заказе уходят по почте и по SMS. Это разные типы, но у обоих есть метод Send. Интерфейс описывает именно это: какие методы должны быть у типа, без подробностей, как они работают.
type Notifier interface {
Send(text string) string
}
type Email struct {
Address string
}
type SMS struct {
Phone string
}
func (e Email) Send(text string) string {
return "Письмо на " + e.Address + ": " + text
}
func (s SMS) Send(text string) string {
return "SMS на " + s.Phone + ": " + text
}
Ни Email, ни SMS нигде не пишут, что они «реализуют» Notifier. У них есть метод Send с нужными параметрами, и этого достаточно: Go сам считает их подходящими.
Теперь одна функция работает с любыми уведомлениями:
func notifyAll(list []Notifier, text string) {
for _, n := range list {
fmt.Println(n.Send(text))
}
}
list := []Notifier{
Email{"anna@example.ru"},
SMS{"+7 900 000-00-00"},
}
notifyAll(list, "заказ готов")
Письмо на anna@example.ru: заказ готов
SMS на +7 900 000-00-00: заказ готов
Если завтра появятся уведомления в мессенджере, notifyAll менять не придётся: достаточно нового типа с методом Send. Интерфейсы в Go маленькие, часто из одного метода, и на них держится вся стандартная библиотека.
Попробуйте сами. Создайте структуру Account с полями Owner и Balance. Добавьте метод Deposit(amount int), который пополняет счёт, и метод Info() string, который возвращает строку вида Иван: 750 ₽. Пополните счёт на 500 и на 250 и выведите информацию.
Решение
package main
import "fmt"
type Account struct {
Owner string
Balance int
}
func (a *Account) Deposit(amount int) {
a.Balance += amount
}
func (a Account) Info() string {
return fmt.Sprintf("%s: %d ₽", a.Owner, a.Balance)
}
func main() {
acc := Account{Owner: "Иван"}
acc.Deposit(500)
acc.Deposit(250)
fmt.Println(acc.Info())
}
Иван: 750 ₽
Deposit меняет баланс, поэтому получатель — указатель *Account. Info только читает данные. fmt.Sprintf работает как Printf, но не выводит строку, а возвращает её.
Мини-тест: проверьте себя
Закрепите урок: несколько вопросов, разбор ответов — в конце.
-
Как обратиться к полю Customer у переменной o типа Order?
К полям структуры обращаются через точку: o.Customer. Так же вызывают и методы: o.Total().
-
Метод объявлен как func (o Order) Add(it Item) и добавляет товар в o.Items. Почему после вызова в заказе ничего не меняется?
Получатель без звёздочки — это копия. Чтобы метод менял сам объект, его объявляют с указателем: func (o *Order) Add(it Item).
-
Есть интерфейс Notifier с методом Send(text string) string. Что нужно, чтобы тип SMS ему соответствовал?
В Go нет слова implements. Если у типа есть все методы интерфейса, он подходит автоматически.
-
Зачем нужны интерфейсы?
Функция notifyAll не знает, письмо перед ней или SMS. Ей достаточно, что у каждого значения есть метод Send.