Если вы собираетесь широко использовать Go, вам необходимо понять, как использовать интерфейсы. Интерфейсы не являются специфической особенностью Go, но Go является одним из наиболее широких пользователей этой функции. Интерфейсы позволяют писать многократно используемый код.
Что такое интерфейсы?
Интерфейсы — это способ группировки объектов по их общему поведению. Интерфейс определяется его именем и методами, которые должны определять объекты. Любой объект, у которого определены эти методы, «реализует» интерфейс.
Например, предположим, что три студента A, B и C умеют готовить. Они пытаются подать мне вкусную еду. Однако, поскольку A, B и C обучались у разных поваров, они готовят по-разному. Тем не менее, все они — повара, умеющие готовить. Мы можем сказать, что A, B и C реализуют интерфейс «шеф-повар», потому что все они умеют готовить по-разному.
Давайте посмотрим, как это можно реализовать в Go.
Как это можно использовать в Go?
Вот код для приведенного выше примера.
package main
import (
"fmt"
"math/rand"
)
type cook interface {
cookFood()
getName() string
}
type chef struct {
name string
cuisine string
}
func (c chef) cookFood() {
result := fmt.Sprintf("%s the professional chef is cooking %s food", c.name, c.cuisine)
fmt.Println(result)
}
func (c chef) getName() string {
return c.name
}
type homeCook struct {
name string
}
func (h homeCook) cookFood() {
result := fmt.Sprintf("%s the home cook is cooking food", h.name)
fmt.Println(result)
}
func (h homeCook) getName() string {
return h.name
}
func serve(c cook) {
c.cookFood()
result := fmt.Sprintf("%s: dinner is served!", c.getName())
fmt.Println(result)
}
func main() {
chef1 := chef{"Brian", "Korean"}
chef2 := chef{"Vincenzo", "Italian"}
homeCook1 := homeCook{"Amara"}
homeCook2 := homeCook{"Dana"}
cooks := []cook{chef1, chef2, homeCook1, homeCook2}
numCustomers := 100
for i := 0; i < numCustomers; i++ {
serve(cooks[rand.Intn(len(cooks))])
}
}
Мы определили интерфейс с именем cook
. Мы хотим, чтобы любой, кто может cookFood
и getName
, мог идентифицировать себя как cook
.
Ниже мы определим две структуры chef
и homeCook
. Обе имеют поле name
, но только chef
имеет поле cuisine
, чтобы определить, на какой кухне он или она специализируется.
Чтобы структуры chef
и homeCook
реализовали интерфейс cook
, им необходимо определить методы cookFood
и getName
. Детали реализации просты — всего лишь простой оператор печати.
Теперь давайте рассмотрим функцию main
. Мы определяем два объекта chef
chef1
и chef2
. Мы также определяем два объекта homeCook
homeCook1
и homeCook2
. Эти четыре объекта хранятся в нашем срезе cooks
с типом cook
, нашим интерфейсом. Обычно вы не можете хранить объекты разных типов в одном срезе, но использование интерфейса позволяет нам это сделать.
Мы вызываем функцию serve
внутри цикла for, которая принимает cook
. Обычно это не работает — нам пришлось бы определить две функции serve
, одну для типа chef
и одну для типа homeCook
. Однако использование интерфейса помогает нам избежать этого повторения.
Бывает трудно оценить интерфейсы, когда ваша кодовая база мала. Небольшие проекты, как правило, не нуждаются в большой структуре, поэтому вы можете обойтись без использования интерфейса. Однако интерфейсы позволяют создавать действительно чистый, предсказуемый код по мере роста вашего проекта.
Как это работает под капотом?
Интерфейсы не только невероятно полезны при проектировании кода, но и очень интересны в своей реализации. Интерфейсы можно описать как два сросшихся блока указателей: один указывает на определение типа, а другой — на его базовое значение. Сбивает с толку, верно? Взгляните на эту строку из примера выше.
chef1 := chef{"Brian", "Korean"}
chef1
реализует интерфейс cook
. Если мы расчленим chef1
в форме интерфейса, то первый указатель будет указывать на определение типа chef
struct, а второй — на фактическое значение chef1
.
Что такое пустые интерфейсы?
Теперь мы можем подумать о более продвинутой или, скорее, более простой концепции: пустой интерфейс. Выше мы говорили, что интерфейс группирует объекты по определенному поведению. Как бы выглядел пустой интерфейс? В нем не было бы методов, которые могли бы быть реализованы для любого типа. Это означает, что объект любого типа может реализовать интерфейс. Это все равно, что сгруппировать живых людей в категорию под названием «организм». Возраст, рост, раса и пол не имеют значения — все люди являются организмами.
Взгляните на этот фрагмент:
func main() {
a := "hello"
b := 100
c := 3.14
objects := []interface{}{a, b, c}
}
Этот код скомпилируется, потому что objects
— это фрагмент, который хранит любой объект, реализующий интерфейс empty.
Советы
В завершение этого поста я хотел бы поделиться с вами некоторыми советами, которые я накопил за свой короткий путь разработки Go.
-
Помните, что, хотя пустые интерфейсы обеспечивают большую гибкость, вы обязаны заботиться о различных возможных типах. Например, предположим, что вы размаршализируете объект JSON. Иногда вы не знаете точную структуру входящего JSON. Go будет ловко использовать тип
map[string]interface{}
для размаршалинга объекта JSON. Ключи JSON будут храниться какstring
, а значения — какinterface{}
. При манипулировании значениями вы должны учитывать все возможные типы, иначе ваш код потерпит поразительное фиаско. Это налог, который приходится платить при работе с пустыми интерфейсами. -
Называйте свои интерфейсы последовательно. Мне очень помогает, когда я называю свои интерфейсы как
--er
. Например,copier
,reader
,parser
и т.д. В стандартной библиотеке есть заметное исключение — интерфейсbuiltin.Error
. Думаю, это нормально, потому что он все еще рифмуется с другими интерфейсами, которые заканчиваются на--er
. -
Определение интерфейса и методов struct/struct в отдельном файле помогает читабельности. Это не всем нравится, но я обнаружил, что это помогает во многих ситуациях.
-
Вам не нужно изобретать колесо. Если вам нужен интерфейс, посмотрите, может быть, стандартная библиотека уже предоставляет его. Некоторые из наиболее распространенных, которые вы можете реализовать, это
fmt.Stringer
,io.Reader
,io.Writer
,builtin.Error
,http.ResponseWriter
,sort.Interface
и т.д. -
Интерфейс — это мощный молоток, но не каждая проблема — это гвоздь. Вам не нужен интерфейс для всего. Иногда накладные расходы и усилия по его созданию не стоят того. Попробуйте создать свое приложение без интерфейса, и вы столкнетесь с ситуацией, когда вам понадобится интерфейс.
Заключение
Надеюсь, эта статья помогла вам прояснить некоторые вопросы, связанные с интерфейсами в Go. Интерфейсы являются неотъемлемой частью языка программирования Go, и вы, несомненно, столкнетесь с ними на своем пути. Когда придет время, я верю, что вы сможете с ними справиться. Продолжайте в том же духе, суслики, и до встречи на следующей неделе с новым постом.
Этот пост также доступен на Medium и на моем личном сайте