Урок 6 из 8 · 25 мин · обновлено

Структуры, методы и интерфейсы

Как описывать свои типы данных через struct, добавлять им методы, почему метод с указателем меняет объект и как интерфейсы позволяют работать с разными типами одинаково.

Структуры

До сих пор каждое значение хранилось в отдельной переменной. Но заказ — это сразу клиент, список товаров и сумма. Чтобы собрать такие данные в один тип, используют структуру:

Go
type Item struct {
	Title string
	Price int
	Qty   int
}

type Order struct {
	Customer string
	Items    []Item
}

type Item struct { … } создаёт новый тип с полями. Поля бывают любого типа, в том числе срезом других структур, как Items.

Значение структуры создают, перечисляя поля, а к полям обращаются через точку:

Go
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 выводит структуру вместе с названиями полей — удобно при отладке.

Методы

Метод — функция, привязанная к типу. Перед её именем в скобках пишут получатель: значение, у которого метод вызывают.

Go
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)
}
Go
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. Интерфейс описывает именно это: какие методы должны быть у типа, без подробностей, как они работают.

Go
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 сам считает их подходящими.

Теперь одна функция работает с любыми уведомлениями:

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 и выведите информацию.

Решение
Go
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, но не выводит строку, а возвращает её.

Мини-тест: проверьте себя

Закрепите урок: несколько вопросов, разбор ответов — в конце.

  1. Как обратиться к полю Customer у переменной o типа Order?

  2. Метод объявлен как func (o Order) Add(it Item) и добавляет товар в o.Items. Почему после вызова в заказе ничего не меняется?

  3. Есть интерфейс Notifier с методом Send(text string) string. Что нужно, чтобы тип SMS ему соответствовал?

  4. Зачем нужны интерфейсы?