Menu
Coddy logo textTech

Правила реализации интерфейсов

Часть раздела Объектно-ориентированное программирование путешествия по 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.

challenge icon

Задание

Легко

Создадим систему конфигурации, демонстрирующую, как указательные и значимые получатели влияют на соответствие интерфейсу. Вы создадите типы, в которых выбор получателя определяет, можно ли использовать с интерфейсом значения, указатели или и то и другое.

Вы организуете код в трёх файлах:

  • 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: Считайте сведения о конфигурации из входных данных и продемонстрируйте правила соответствия интерфейсу:
    1. Создайте ReadOnlyConfig и передайте указатель в ProcessConfig (это необходимо, потому что SetValue имеет указательный получатель)
    2. Создайте 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
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор Go