Правила реализации интерфейсов
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 43 из 107.
Хотя Go использует утиную типизацию для неявного удовлетворения интерфейсов, существуют определенные правила, которые определяют, действительно ли тип удовлетворяет интерфейсу. Понимание этих правил поможет вам избежать трудноуловимых ошибок.
Самое важное правило касается указателей-получателей. Если метод определен с указателем-получателем, только указатель на этот тип удовлетворяет интерфейсу — не само значение:
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() имеет указатель в качестве получателя (pointer receiver), только *Document удовлетворяет интерфейсу Saver:
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // работает — указатель удовлетворяет интерфейсу
// Process(doc) // ошибка компиляции — значение не удовлетворяет интерфейсу
}
Однако обратное утверждение более гибкое. Если метод имеет получатель-значение (value receiver), и типы значений, и типы указателей удовлетворяют интерфейсу. Go автоматически разыменовывает указатели при вызове методов с получателем-значением:
func (d Document) Info() string { // получатель значения
return d.Name
}
// И Document, и *Document удовлетворяют интерфейсу, требующему Info()
Эта асимметрия существует, потому что Go всегда может получить значение из указателя (путем разыменования), но он не всегда может получить указатель из значения (значение может быть неадресуемым). Соблюдение этого правила помогает избежать запутанных ошибок компилятора при работе с интерфейсами.
Задание
ЛегкоДавайте создадим систему конфигурации, которая демонстрирует, как получатели (receivers) в виде указателей и значений влияют на соответствие интерфейсу. Вы создадите типы, в которых выбор получателя определяет, можно ли использовать значения, указатели или и то, и другое с интерфейсом.
Вы организуете свой код в трех файлах:
config.go: Определите интерфейсConfigurable, который требует два метода:GetValue() stringиSetValue(string). Затем создайте два типа конфигурации:ReadOnlyConfigс полемValue— используйте получатель-значение дляGetValue()(возвращает Value) и получатель-указатель дляSetValue()(обновляет Value)Settingс полемData— используйте получатели-значения для обоих методов (GetValue возвращает Data, SetValue просто выводит "Cannot modify" без каких-либо изменений)
processor.go: Создайте функцию с именемProcessConfig, которая принимаетConfigurableи новую строку значения. Она должна вывести текущее значение с помощьюGetValue(), вызватьSetValue()с новым значением, а затем снова вывести значение, чтобы показать любые изменения.main.go: Считайте детали конфигурации из входных данных и продемонстрируйте правила соответствия интерфейсу:- Создайте
ReadOnlyConfigи передайте указатель вProcessConfig(требуется, так какSetValueимеет получатель-указатель) - Создайте
Settingи передайте значение напрямую вProcessConfig(работает, так как оба метода имеют получатели-значения)
- Создайте
Будут предоставлены следующие входные данные:
- Строка 1: Начальное значение для ReadOnlyConfig
- Строка 2: Новое значение для установки в ReadOnlyConfig
- Строка 3: Начальное значение для Setting
- Строка 4: Новое значение для попытки установки в Setting
Ваша функция 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 работают напрямую, потому что все его методы используют получатели-значения.
Шпаргалка
Go использует утиную типизацию для неявной реализации интерфейсов, но существуют определенные правила относительно получателей-указателей и получателей-значений:
Правило получателя-указателя
Если метод имеет получатель-указатель, только указатель на этот тип удовлетворяет интерфейсу:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // получатель-указатель
return "Saved: " + d.Name
}
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // работает - указатель удовлетворяет интерфейсу
// Process(doc) // ошибка компиляции - значение не удовлетворяет интерфейсу
}Правило получателя-значения
Если метод имеет получатель-значение, интерфейсу удовлетворяют как типы значений, так и типы указателей:
func (d Document) Info() string { // получатель-значение
return d.Name
}
// И Document, и *Document удовлетворяют интерфейсу, требующему Info()Почему существует эта асимметрия
Go всегда может получить значение из указателя (путем разыменования), но не всегда может получить указатель из значения (значение может быть неадресуемым).
Попробуйте сами
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 имеет получатель-указатель)
// TODO: Создайте Setting со значением settingInitial
// TODO: Передайте ЗНАЧЕНИЕ напрямую в ProcessConfig (работает, так как оба метода имеют получатели-значения)
// Используйте эти переменные, чтобы избежать ошибок из-за неиспользуемых переменных
_ = 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