Правила реализации интерфейсов
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy. Урок 43 из 107.
Хотя Go использует утиную типизацию для неявного соответствия интерфейсу, существуют определённые правила, которые определяют, действительно ли тип соответствует интерфейсу. Понимание этих правил поможет вам избежать скрытых ошибок.
Самое важное правило касается pointer receivers. Если метод определён с pointer receiver, только указатель на этот тип удовлетворяет интерфейсу, а не само значение:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // получатель-указатель
return "Saved: " + d.Name
}
func Process(s Saver) {
fmt.Println(s.Save())
}
Поскольку у Save() получатель-указатель, только *Document удовлетворяет интерфейсу Saver:
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // работает - указатель удовлетворяет интерфейсу
// Process(doc) // ошибка компиляции - значение не удовлетворяет интерфейсу
}
Однако обратный вариант более гибкий. Если у метода получатель значения, both тип значения и тип pointer удовлетворяют интерфейсу. Go автоматически разыменовывает указатели при вызове методов с получателем значения:
func (d Document) Info() string { // получатель по значению
return d.Name
}
// И Document, и *Document удовлетворяют интерфейсу, требующему Info()
Эта асимметрия существует потому, что Go всегда может получить значение из pointer (разыменовав его), но не всегда может получить pointer из значения (у значения может не быть адреса). Если помнить об этом правиле, можно избежать непонятных ошибок компилятора при работе с interface.
Задание
ЛегкоСоздадим систему конфигурации, демонстрирующую, как указательные и значимые получатели влияют на соответствие интерфейсу. Вы создадите типы, в которых выбор получателя определяет, можно ли использовать с интерфейсом значения, указатели или и то и другое.
Вы организуете код в трёх файлах:
config.go: Define интерфейсConfigurable, требующий два метода:GetValue() stringиSetValue(string). Затем создайте два типа конфигурации:ReadOnlyConfigс полемValue: используйте значимый получатель дляGetValue()(возвращает Value) и указательный получатель дляSetValue()(обновляет Value)Settingс полемData: используйте значимые получатели для обоих методов (GetValue возвращает Data, SetValue просто выводит "Cannot modify", ничего не изменяя)
processor.go: Создайте function с именемProcessConfig, которая accepts интерфейсConfigurableи новую строку значения. Она должна вывести текущее значение с помощьюGetValue(), вызватьSetValue()с новым значением, а затем снова вывести значение, чтобы показать любые changes.main.go: Считайте сведения о конфигурации из входных данных и продемонстрируйте правила соответствия интерфейсу:- Создайте
ReadOnlyConfigи передайте указатель вProcessConfig(это необходимо, потому чтоSetValueимеет указательный получатель) - Создайте
Settingи передайте значение напрямую вProcessConfig(это работает, потому что оба метода имеют значимые получатели)
- Создайте
Будут предоставлены следующие входные данные:
- Строка 1: Исходное значение для ReadOnlyConfig
- Строка 2: Новое значение, которое нужно установить для ReadOnlyConfig
- Строка 3: Исходное значение для Setting
- Строка 4: Новое значение, которое нужно попытаться установить для Setting
Ваша function ProcessConfig должна выводить данные в следующем формате:
Current: [value]
Current: [value after SetValue]Например, если заданы debug, production, localhost и remote, вывод должен быть следующим:
Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhostОбратите внимание: ReadOnlyConfig действительно изменяет своё значение, потому что мы передали указатель, тогда как Setting остаётся неизменным (его SetValue со значимым получателем не может изменить исходное значение). Главный вывод заключается в том, что одни только значения ReadOnlyConfig не соответствуют Configurable — соответствуют только указатели, тогда как значения Setting работают напрямую, потому что все его методы используют значимые получатели.
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Прочитать входные данные
scanner.Scan()
rocInitial := scanner.Text()
scanner.Scan()
rocNew := scanner.Text()
scanner.Scan()
settingInitial := scanner.Text()
scanner.Scan()
settingNew := scanner.Text()
// TODO: Создать ReadOnlyConfig со значением rocInitial
// TODO: Передать УКАЗАТЕЛЬ в ProcessConfig (требуется, потому что SetValue имеет pointer receiver)
// TODO: Создать Setting со значением settingInitial
// TODO: Передать ЗНАЧЕНИЕ напрямую в ProcessConfig (работает, потому что оба метода имеют value receivers)
// Использовать эти переменные, чтобы избежать ошибок неиспользуемых переменных
_ = rocInitial
_ = rocNew
_ = settingInitial
_ = settingNew
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker PoolПотренируйтесь самостоятельно: Онлайн-компилятор Go