Menu
Coddy logo textTech

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

Часть раздела Объектно-ориентированное программирование путешествия по 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 всегда может получить значение из указателя (путем разыменования), но он не всегда может получить указатель из значения (значение может быть неадресуемым). Соблюдение этого правила помогает избежать запутанных ошибок компилятора при работе с интерфейсами.

challenge icon

Задание

Легко

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

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

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