На этом шаге рассмотрим использование интерфейсов в Go.
Непустой интерфейс сам по себе обычно не используется. Чтобы получить от него хоть какую-то пользу, необходимо создать несколько пользовательских типов с методами, определяемыми интерфейсом. Ниже приводятся два таких типа.
type ПараСтрок struct{первая, вторая string} func (пара *ПараСтрок) Exchange() { пара.первая, пара.вторая = пара.вторая, пара.первая } type Точка [2]int func (точка *Точка) Exchange() { точка[0], точка[1] = точка[1], точка[0] }
Типы ПараСтрок и Точка совершенно не похожи друг на друга, но, так как оба реализуют метод Exchange(), они оба удовлетворяют требованиям интерфейса Exchanger. Это означает, что можно создать значения типов ПараСтрок и Точка и передавать их функциям, принимающим значения типа Exchanger.
Несмотря на то что оба типа, ПараСтрок и Точка, реализуют интерфейс Exchanger, это никак явно не отмечено в программном коде. Простого факта, что типы ПараСтрок и Точка реализуют методы (в данном случае единственный метод) интерфейса, уже достаточно для компилятора Go, чтобы считать эти значения соответствующими данному интерфейсу.
Приемники передаются методам по указателям на значения их типов, поэтому методы могут изменять значения, относительно которых они вызываются.
Даже при том, что функции вывода в языке Go способны выводить значения пользовательских типов, обычно предпочтительнее бывает организовать более полный контроль над их строковым представлением. Этого легко можно добиться добавлением реализации метода, определяемого интерфейсом fmt.Stringer, то есть метода с сигнатурой String() string.
func (пара ПараСтрок) String() string { return fmt.Sprintf("%q+%q", пара.первая, пара.вторая) }
Этот метод возвращает строку, содержащую значения строковых полей в двойных кавычках со знаком "+" между ними. При наличии такого метода функции вывода из пакета fmt будут использовать его для вывода значений типа ПараСтрок, а также *ПараСтрок, потому что компилятор Go автоматически разыменовывает такие указатели, чтобы получить значение, на которое он указывает.
Следующий фрагмент демонстрирует создание нескольких значений типа Exchanger, несколько вызовов метода Exchange() и несколько вызовов пользовательской функции exchangeThese(), принимающей значения типа Exchanger.
строка1 := ПараСтрок{"Иванов", "Иван"} строка2 := ПараСтрок{"Степан", "Степанов"} точка := Точка{5, -3} fmt.Println("До обмена: ", строка1, строка2, точка) строка1.Exchange() /* Все переменные создаются как значения, даже при том, что методы Exchange() требуют передачи приемника по указателю. Компилятор Go автоматически передает адрес значения при вызове метода, требующего указатель, если значение, относительно которого производится вызов, является адресуемым. Интерпретируется как: (&строка1).Exchange()*/ строка2.Exchange() // Интерпретируется как: (&строка2).Exchange() точка.Exchange() // Интерпретируется как: (&точка).Exchange() fmt.Println("После первого обмена:", строка1, строка2, точка) exchangeThese(&строка1, &строка2, &точка) fmt.Println("После второго обмена:", строка1, строка2, точка)
Результат выполнения программы можно увидеть на рис.1
Рис.1. Результат работы программы
Архив с примерами можно взять здесь.
Функции exchangeThese() явно должны передаваться адреса значений. Если, к примеру, передать ей значение строка2 типа ПараСтрок, компилятор Go заметит, что тип ПараСтрок не соответствует интерфейсу Exchanger, поскольку он не определяет метод Exchange() с приемником типа ПараСтрок, и прервет компиляцию с сообщением об ошибке. Однако, если передать значение типа *ПараСтрок (например, &строка2), компиляция будет выполнена успешно. Это обусловлено тем, что, согласно объявлению, метод Exchange() ожидает получить приемник типа *ПараСтрок, а это означает, что тип *ПараСтрок соответствует интерфейсу Exchanger.
Приведем реализацию функции exchangeThese().
func exchangeThese(exchangers ...Exchanger) { for _, exchanger := range exchangers { exchanger.Exchange() } }
Эта функция понятия не имеет, что ей передаются два указателя типа *ПараСтрок и один типа *Точка. Единственное, что она требует, чтобы передаваемые ей параметры реализовали интерфейс Exchanger, – это требование проверяется компилятором, поэтому используемая здесь динамическая типизация безопасна по отношению к типам.
На следующем шаге продолжим рассматривать использование интерфейсов в Go.