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

Go не поддерживает традиционные конструкторы по умолчанию, но предоставляет конструкторские фабричные функции, полезные для инициализации типов.

Этот пост фактически является частью II, где я в основном делаю 180 по своей первоначальной идее в сообщении, указанном выше. Но сначала, почему оригинальный пост?

Избегайте нулевых указателей

Go имеет много сильных сторон как язык: от простого API параллелизма пользовательской среды до очень быстрой настройки микросервиса, обменивающегося данными по HTTP только с помощью встроенного веб-сервера, который готов к производству, или очень легко через RPC с другими микросервисами с помощью что-то вроде протобуфа.

Для меня тот факт, что Go является языком со строгой типизацией, также является сильной стороной - вы должны четко указывать, что вы создаете, и это на самом деле упрощает чтение ваших программ. Я также начинаю видеть преимущества необходимости в небольших неявных интерфейсах, определенных рядом с потребителями. Это большая часть SOLID - разделение интерфейсов. Фактически, моя команда и я планируем удалить все явные интерфейсы из кодовой базы.

С другой стороны, как и у всех языков, у Go тоже есть некоторые недостатки. Одним из них является возможность инициализировать структуру в «недопустимом состоянии», что может привести к случайному разыменованию нулевого указателя в дальнейшем.

Обратите внимание, как в приведенном выше примере использование Службы {} позволяет инициализировать Службу без зависимостей. И нет веских причин, по которым эти зависимости не должны существовать.

Частично это можно смягчить с помощью техники, называемой «создание полезного нулевого значения», которая включает в себя фактическое разрешение вашему коду вызывать методы для нулевых указателей и возвращение пустой строки или структуры или любого другого возвращаемого вами типа. Итак, теперь, если по какой-то причине ваш Сервис равен нулю или репозиторий в нем равен нулю, вы просто возвращаете вместо этого значение по умолчанию.

Таким образом, вместо того, чтобы выяснять, что именно не так с вашим кодом во время компиляции, реальной силой использования компилируемого языка, такого как Go, вы обнаруживаете не только то, что что-то не так где-то в приложении, но и также, что вы не совсем уверены, где и почему он находится, и поэтому следует отладка по всему стеку.

Кажется, «полезно» либо в глазах смотрящего, либо евангелиста; не обязательно в глазах разработчика программного обеспечения.

Обеспечение допустимого состояния

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

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

С указанным выше вы больше не можете писать s: = Service {} или r: = Repository {}, потому что эти структуры не экспортируются. Единственный способ получить к ним доступ - это вызвать New (), который возвращает только экспортированный интерфейс, содержащий те же методы, что и структура. Дополнительным моментом здесь является то, что Служба не может существовать без зависимости, не допускающей исключения (мы не позволяем передавать здесь указатель).

Эта стратегия эффективно применяет конструктороподобную семантику, которую можно увидеть в других языках - единая, строго контролируемая точка входа для инициализации, которую нельзя обойти (за исключением пакетов отражения и небезопасных пакетов, что выходит за рамки данной статьи и, вероятно, будет нелегко. все равно пропустить проверку кода).

У этого подхода было несколько недостатков. Одним из них было введение дублированного кода, потому что мы создавали явный интерфейс рядом с каждой отдельной структурой в базе кода, просто чтобы обеспечить то, что должно было быть в языке по умолчанию и может даже не происходить так регулярно. Еще одним недостатком было то, что godoc не работал должным образом с методами, описанными в интерфейсе.

Фактически, при использовании этого в команде это доставляло больше хлопот, чем того стоило. Все согласились с тем, что нам не нужны «полезные нулевые значения», поскольку бизнес не имеет для них концепции, и поэтому единственная причина, по которой мы будем использовать «нулевые значения», будет состоять в том, чтобы удовлетворить компилятор и недостатки языка, и просто состоят из «кода-заполнителя», поэтому во многих случаях мы решили их не указывать. На самом деле это был лучший результат для нас, поскольку нам больше не нужно было заботиться о нулевых значениях, если они не были абсолютно необходимыми и преднамеренными.

Что сработало для нас лучше всего

Конечный результат был согласован: всегда использовать новое соглашение для каждой структуры. К сожалению, это не могло быть реализовано на уровне языка, но простой статический анализ на CI и линтер могли проверить инициализацию структур, которые не использовали фабричный метод, в любом файле, который не был тестовым, и это в конечном итоге обеспечило лучшее баланс для нас (тесты могут использовать ярлыки при необходимости).

У этого подхода было четыре преимущества:

  • Каждая структура имеет одну точку инициализации и проверки, поэтому всегда будет действительна после инициализации, просто за счет фокусирования на New ().
  • Шансы нахождения нулевых указателей только во время выполнения были значительно уменьшены.
  • Подход «значения по умолчанию» никогда не нужно рассматривать во всей кодовой базе, которая устраняет множество связанных когнитивных накладных расходов, необходимых для работы с чем-то, что фактически является проприетарным в Go.
  • Разработчики-новички на Go смогли быстрее внедрить кодовую базу и работать с ней, потому что им не нужно было разбираться в ненужных тонкостях.

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

Было предложено, чтобы я в следующий раз взглянул на Rust. У этого языка также будут свои недостатки, я уверен, но, возможно, он мог бы больше соответствовать философии `` безопасность по умолчанию '', в которой я начинаю видеть реальную ценность, и это то, что я определенно думаю, должно стать целью для многих. языки, к которым нужно стремиться.

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

Первоначально опубликовано на http://blog.j7mbo.com 29 декабря 2019 г.