Какие свойства полей структуры определяют, можно ли сравнивать её значения оператором == в Go?
Структура сравнима оператором ==, если сравним каждый её полевой тип. Поля-срезы, отображения и функции делают структуру несравнимой; наличие хотя бы одного такого поля приводит к ошибке компиляции при сравнении значений структуры.
Go стремится сделать базовые операции типизированными и предсказуемыми на этапе компиляции. Правило сравнимости позволяет использовать значения подходящих структур как ключи map без специального пользовательского хеширования.
Такой подход отделяет сравнение значений от глубокого сравнения ссылочных структур. Для срезов, отображений и функций язык не задаёт обычное сравнение содержимого или поведения, поэтому они не участвуют в сравнении структур через ==.
При проектировании структуры важно понимать, будет ли она пригодна для сравнения или использования в качестве ключа отображения. Добавление поля-среза, отображения или функции может неожиданно сделать весь тип несравнимым и сломать существующий код на этапе компиляции.
Нельзя считать структуру сравнимой только потому, что её поля имеют простые значения в конкретном экземпляре. Сравнимость определяется типами полей, а не их текущим содержимым.
Сравнимыми являются, в частности, bool, числовые типы, строки, указатели, каналы и интерфейсы. Массив сравним, если сравним его тип элемента, а структура — если сравним каждый её полевой тип.
Срезы, отображения и функции нельзя сравнивать между собой оператором ==; для них допустимо только сравнение с nil в соответствующих контекстах. Поэтому структура с полем типа среза, отображения или функции также не является сравнимой.
Важна рекурсивность правила: если структура содержит массив, нужно проверить тип элемента массива; если содержит другую структуру — сравнимость всех её полей. Типы полей учитываются независимо от того, экспортируются они или нет.
Интерфейсный тип сам по себе допускает сравнение, но при сравнении двух интерфейсных значений возможна паника, если динамическое значение внутри интерфейса несравнимо. Это отличается от структуры с полем интерфейсного типа: такая структура считается сравнимой на уровне типов, однако конкретное сравнение всё равно может завершиться паникой из-за динамического значения интерфейса.
Сравнение сравнимых структур выполняется по полям в порядке их объявления. Оно не является глубоким сравнением произвольных ссылочных данных: например, указатели сравниваются по адресам, а не по содержимому объектов.
В конфигурации сервиса была структура с идентификатором, регионом и набором тегов. Команда хотела сравнивать конфигурации оператором ==, но после добавления поля тегов тип перестал быть сравнимым, а использование конфигурации как ключа map стало невозможным.
Рассматривались два варианта. reflect.DeepEqual позволил бы сравнивать содержимое срезов, но добавил бы менее явную семантику и отражение во время выполнения. Ручной метод сравнения потребовал бы больше кода, зато дал бы контроль над тем, считать ли nil и пустой срез равными.
Выбрали явный метод сравнения для конфигурации. Он документировал бизнес-правила, позволил отдельно обработать порядок элементов и различие между отсутствующим и пустым набором, а также не связывал модель с возможностью использования в качестве ключа map.
Вопрос: Можно ли сравнивать две структуры, содержащие поле интерфейсного типа?
Ответ: Да, такая структура считается сравнимой, потому что интерфейсный тип является сравнимым. Но во время сравнения интерфейсных полей возможна паника, если в них находятся динамические значения несравнимого типа, например срезы или отображения. Поэтому статическая сравнимость структуры не гарантирует безопасное сравнение любых значений во время выполнения.
Вопрос: Что именно сравнивается в структуре с указательным полем?
Ответ: Сравниваются сами указатели, то есть равенство их значений, обычно адресов или nil. Два указателя на разные объекты с одинаковым содержимым не равны, если они не указывают на один и тот же объект. Для сравнения содержимого нужно явно разыменовать указатели и сравнить полученные значения, если их тип допускает такую операцию.
Вопрос: Почему структура с массивом байтов может быть ключом map, а структура со срезом байтов — нет?
Ответ: Массив является самостоятельным значением фиксированного размера и сравним, если сравним его элемент. Срез — это дескриптор изменяемой последовательности, и язык не определяет сравнение двух срезов по их элементам через ==. Поэтому структура с массивом байтов может быть сравнимой и использоваться как ключ, а структура со срезом байтов — нет.