Есть много хороших обсуждений интерфейсов в Go, некоторые из них мои, но я хочу представить более абстрактную статью о практических аспектах. Для этого сначала нам нужно обсудить концепцию интерфейсов в языках программирования.
Что такое интерфейсы в программировании?
Концептуально интерфейс — это абстрактное определение конкретных наборов поведений, которые должны быть чем-то реализованы. Язык может полностью игнорировать эту концепцию или реализовать ее с помощью таких вещей, как интерфейсы, протоколы, утиная типизация или другие механизмы.
Рассмотрим систему выставления счетов, чтобы создать счет, вам нужно знать только цену товара и облагается ли он налогом. Один из подходов к этому состоит в том, чтобы определить интерфейс Invoiceable, описывающий два поведения, Price и Taxable, и любой элемент, который вы хотите добавить в счет, автомобиль, банан, утку, просто должен реализовать Invoiceable. Концептуально это можно сделать во время выполнения, когда элемент динамически опрашивается на предмет совместимости, или во время компиляции, когда компилятор должен иметь возможность определить, что элемент реализует интерфейс. Пара хорошо известных конкретных примеров этого — интерфейс Iterable в Java, реализованный коллекциями для предоставления унифицированного доступа к содержимому, или интерфейс Stringer в Go, реализованный всем, что хочет предложить строковое представление.
Что примечательного в интерфейсах в Go
Go предоставляет интерфейсы, которые применяются во время компиляции. Если вы укажете, что что-то должно быть Invoiceable в вашем коде, этот код будет скомпилирован, только если элемент действительно реализует интерфейс Invoiceable. Есть способы, которыми вы можете подорвать это, но в целом можно доверять этому утверждению.
Что примечательно в реализации Go, так это то, что она не требует, чтобы элемент был явно помечен как реализующий Invoiceable, как, например, в Java, но опрашивает элемент во время компиляции.
Плюсы
Подход Go имеет то преимущество, что существование интерфейса не связано с элементами, к которым вы его применяете. Опрашиваемый элемент не должен заранее знать об интерфейсе. Вы можете определить Invoiceable в своем коде и протестировать что-то из стороннего кода, и если элемент реализует поведение Invoiceable, код будет компилироваться и работать правильно. Это очень гибкий подход, который можно использовать во время компиляции.
Минусы
Раздельный подход Go также может быть проблематичным для поддержки кода. Рассмотрим сценарий, в котором третья сторона глобально переименовывает свой метод Price в ItemPrice. Компилятор подхватит и сообщит, что их элементы больше не являются Invoiceable, однако об этом будет сообщено повсюду, где бы вы ни использовали интерфейс с их кодом. Это может быть довольно подавляющим и разбросанным, что затрудняет изолирование проблемы.
Для этой проблемы можно принять процедурное исправление, которое заключается в том, чтобы явно, в логическом централизованном месте сделать следующее:
var (
_ Invoiceable = (*Vehicle)(nil)
_ Invoiceable = (*Truck)(nil)
_ Invoiceable = (*Part)(nil)
)
Это не волшебство, это просто создание центрального места, где компилятор будет опрашивать сторонние элементы на соответствие интерфейсу Invoiceable. Хотя вы по-прежнему будете получать все разрозненные ошибки на компиляторы, теперь вы получите крупную ошибку откуда-то, которая немедленно подскажет вам суть проблемы.