Только не экспортируй приватные типы в Go

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

Неэкспортируемые типы в Go: откуда все началось

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

Но структуры в Go – это не то же самое, что классы и объекты в других языках. Они играют похожую роль, но не такую же!
Кстати, узнайте больше о нашем подходе в вебразработке, также рекомендуем посмотреть наши кейсы – они точно вас удивят.

Проблемы с экспортом приватных типов

Основная проблема с экспортированием приватных типов заключается в том, что это нарушает принцип инкапсуляции.

Само название "неэкспортируемая структура" говорит: она для внутреннего использование пакета. И нужна, чтобы потребители кода работали ТОЛЬКО с экспортируемыми типами. Это инкапсуляция, которая позволяет скрывать то, куда не надо лезть извне! То, что работает сложным образом, а также может изменяться с новыми версиями пакета. Поэтому делать такое - то же самое, что пытаться сделать что-то маркируемое "несъедобное" - съедобным. Это забивать гвозди перфоратором. Если программист так делает, то он просто не понимает зачем это нужно.

Написание структур, которые не требуют инициализации.

Примеры: sync.Mutex, time.Time, sync.WaitGroup и другие.

При дефолтной инициализации они будут работать корректно. Можно использовать именованные конструкторы для инициализации в нужное состояние: time.Now(), time.Parse(). Это идеальный подход! Но такого можно достичь не во всех случаях.

Сложный доменный сервис может потребовать более глубокой инициализации

Например, инъецировании зависимостей (Config, Logger, Repo, NetClient и т.п.). В таком случае создаётся именованный конструктор и структура с приватными полями, в которые вообще ничего нельзя записать при базовой инициализации. В этом случае зависимости, конфигурация и прочее проверяются в конструкторе, который может вернуть ошибку.

NewDomainService(c Config, d Dependencies) (*DomainService, error)

У таких сервисов в сигнатуре всегда будут методы, которые выполняют какую-то доменную функцию:

  • Start(ctx context.Context) error
  • Process(ctx context.Context) error
  • Execute(ctx context.Context) error
  • .....

Если сервис не был корректно инициализирован, то они вернут ошибку. В 99% случаев такие сервисы и так могут вернуть разные ошибки по десяткам причин. Так что аргумент "мне лень обрабатывать ошибки" - звучит плохо.

Обрабатывать ошибки – это подход Go.

Это делается двумя строчками, которые генерируются IDE автоматически уже. И именно это позволяет сразу понимать узкие места своего кода, возможные ошибки и заставляет думать что с ними делать. И ничего страшного при этом не случиться.

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

Если же подобная проблема (мой сервис не был инициализирован) приводит к "каким-то страшным критическим вещам", например, удалилась база данных, отформатировался диск, перевелись биткоины в несуществующий кошелёк, то у вас проблемы с написанием сервисом! Проблема в Вас, а не в том, кто использует ваш код! Потому что так случаться никогда не должно!

Защита от неведомых проблем с "глупыми потребителя" – жди проблем

А именно:

  1. Вы не сможете построить нормальную архитектуру приложения. Потому что вы просто не можете использовать этот приватный тип в слоях верхнего уровня. Например, если это репозиторий, или какой-то другой доменный сервис, то его надо описать в интерфейсе потребителя. Но вы не можете этого сделать, потому что это приватный тип! Вы просто не сможете им типизировать что-то за пределами его пакета! Поэтому DI работать не будет.
  2. Если вы хотите подсунуть его в интерфейсный тип какого-то потребителя, то вам понадобится следующее:

    -ServiceLocator или ApplicationRegistry - это объект верхнего уровня архитектуры, который хранит ссылки на основные сервисы. Он должен знать хранить инициализацию этого объекта.

    -IoС - тут будет необходимость забиндить реализацию для интерфейса, который будет автоматически инъецироваться при билдинге объектов. Будете ли вы это делать руками, использовать Wire или FX, вам всё равно надо написать, например, FX Show конструктор для этого типа, а потом забиндить его к нужным интерфейсам. Всё это невозможно сделать с приватными типами.
  3. Вы не сможете использовать документацию GoDoc, потому что приватные типы там не описываются для внешнего использования, что абсолютно логично!
  4. Противоречит идиоматике языка и путает других разработчиков нестандартным и неправильным подходом.

Вместо заключения

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

Если вам нужна консультация по поводу разработки на Gо, или вы хотите доработать свое приложение – обращайтесь, подскажем лучшие варианты решения вашей задачи.