Большим достижением в кодировании является написание кода для других разработчиков программного обеспечения, чтобы они могли создавать вокруг него свой код. Для этого, чтобы сделать желаемый уровень абстракции достаточно хорошим для повторного использования, иногда мы используем Reflection. Моим первым приложением на любом языке программирования было Hello World. Вторым было тестирование Reflection. То же самое произошло с Голангом. Будучи еще неопытным разработчиком Golang, я искал библиотеку, которая позволила бы мне динамически адаптировать Golang Structs во время выполнения. Удивительно, что библиотеки еще не существовало, по крайней мере для меня. Однажды я начал копать и предложил возможное решение. Это был хороший кусок кода. Не отлично, но и не ужасно. Через пару месяцев он стал популярным. На сегодняшний день это самый многоразовый код, вышедший из моих рук. Но с этой библиотекой есть только одна проблема — я никогда ею не пользовался.

Что можно сделать с помощью Reflection в Golang?

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

В приведенном выше примере есть некоторые особенности библиотеки динамических структур. Полный список этих функций практически ограничен тем, что действительно может сделать Reflection in Golang.

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

Новые динамические структуры изначально создаются в базовой функции Golang StructOf из пакета Reflect. Это дает возможность определить имена полей, их теги, порядок, путь к пакету. Практически на выходе вы получаете (почти) классический экземпляр структуры Golang. Разве что инициализация у них намного медленнее, чем у классических. И что к ним нельзя прицепить функции (бывал там, такое пробовал), поэтому они не могут выполнять ни один интерфейс. Но в данном случае это рабочее решение. Если вы не ожидаете миллионов запросов в секунду, вероятно, этого будет достаточно, чтобы однажды начать работу.

Так в чем же проблема?

Почему следует избегать использования Reflection в Golang?

Как только я нашел тему на Reddit, пользователь dchapes добавил свое честное мнение о моей библиотеке. Он абсолютно прав. В принципе, я понимаю, почему мы не должны делать что-то подобное в Голанге. Это своего рода инстинкт. Простое ощущение, что что-то не так. Это интуиция, которую вы формируете во время работы с Golang. Вы узнаете, что такое Путь Голанг.

Позвольте мне быть ясным. Не возбраняется использовать Reflection целиком. В конце концов, создание динамической структуры было не единственной частью, которая полагается на Reflection в предоставленном фрагменте кода. Обе основные функции, Marshal и Unmarshal, полагаются на него. На него также опирается функция Структура из пакета Валидатор. Практически весь фрагмент кода является примером использования Reflection, почти в каждой строке.

Кроме того, библиотека dynamic-struct не использует некоторые неприятные хаки для получения окончательных результатов. Он просто использует основную функцию Golang для создания структур, а затем их экземпляров. Эта функция настолько заметна, что сам факт того, что никто раньше не создавал подобную библиотеку, был для меня достаточно удивительным.

Мы знаем, что частью Пути Голанга является использование генераторов кода. Честно говоря, как человек, начавший с Golang с опытом работы со скриптовыми языками, я с самого начала скептически отнесся к использованию генераторов. Потребовалось некоторое время, чтобы осознать все его преимущества. Сегодня для меня это must have. Насмешки, создание схем БД или чванливых клиентов API или пользовательских картографов. Но все генераторы или, по крайней мере, большинство из них зависят от основного пакета Golang AST. И этот пакет, на мой взгляд, Голанг Рефлексия на стероидах.

Итак, в чем проблема с Reflection? Ну… Он медленный. Это менее читабельно. Это труднее понять. Это не легко отладить. Он обращается к местам в памяти, которые, вероятно, не должны касаться. Неважно, насколько хорошо мы сможем написать свой код, но решение с Reflection всегда выглядит некрасиво. И не имеет значения, насколько быстро вы можете предоставить некоторый код с помощью Reflection, когда решение без него может быть чище, быстрее, проще в сопровождении и элегантнее.

Если ваш код можно переписать без Reflection за разумное время и с разумным объемом кода, всегда выбирайте это решение.

Итак, как решить проблему без Reflection?

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

Тем не менее, этот код зависит от функций из других пакетов, которые полагаются на Reflection, таких как JSON и Validator. Мы все еще можем жить с этим, поскольку эти пакеты некоторое время поддерживаются большим и опытным сообществом. Важно, чтобы критическая логика была написана более понятно. Кто-то еще, кто хочет исправить ошибку, не должен углубляться в создание динамических структур, а работать с простыми картами и циклами. Чтобы иметь возможность читать и исправлять простые блоки кода. Не для того, чтобы проверить уровень кода ядерной физики и начать плакать.

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

Фрагмент кода выше является достаточно хорошей заменой Reflection. Но любая замена Reflection достаточно хороша.

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

Вывод

Golang создан для того, чтобы не давать слишком много свободы при работе с Reflection, и на это есть причина. Использование Reflection на любом языке иногда является единственным решением, которое мы можем предложить. Но, иногда, это просто короткий путь. Многие разработчики выбирают его как направление для написания кода. Даже это решение без него возможно, просто потому, что подход в финальном коде кажется проще с Reflection. Именно поэтому Reflection от Golang такой плохой — он заставляет нас дважды подумать, прежде чем использовать его. И заставить нас писать лучший код, несмотря на нашу лень.

Теперь я всегда дважды думаю, прежде чем использовать Reflection (в Golang).

Что вы думаете о Рефлексии? Вообще и/или в Голанге? Не стесняйтесь писать свои мысли в комментариях.