TGViewer
Библиотека Go-разработчика | Golang Библиотека Go-разработчика | Golang @goproglib · 24.1K subscribers
Post #7346 2.73K
👨‍💻 Сравнение двух Go-структур без хардкода полей. Часть 4, указатели, nil и выводы

В прошлой части мы дошли до момента, где надо сравнить два поля. На этом месте чаще всего ломаются наивные реализации. Реальные модели базы полны *string, *int64, *time.Time. Указатель может быть nil, то есть «значения нет».

Сравнение указателя с не-указателем, или двух указателей, где один nil, требует аккуратности:
dtoIsPtr := dtoField.Kind() == reflect.Ptr
dbIsPtr := dbField.Kind() == reflect.Ptr

if dtoIsPtr && dbIsPtr {
if dtoField.IsNil() && dbField.IsNil() {
continue // оба nil, ничего не изменилось
}
if dtoField.IsNil() != dbField.IsNil() {
// одна сторона nil, другая нет, это точно изменение
if dtoField.IsNil() {
updateMap[dbGormColumnName] = nil
} else {
updateMap[dbGormColumnName] = dtoField.Elem().Interface()
}
continue
}
// оба не nil, разворачиваем и сравниваем значения
dtoField = dtoField.Elem()
dbField = dbField.Elem()
} else if dtoIsPtr || dbIsPtr {
// смешанный случай, одна сторона указатель, другая обычное значение
if dtoIsPtr {
if dtoField.IsNil() {
updateMap[dbGormColumnName] = nil
continue
}
dtoField = dtoField.Elem()
}
if dbIsPtr {
if dbField.IsNil() {
updateMap[dbGormColumnName] = dtoField.Interface()
continue
}
dbField = dbField.Elem()
}
}


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

Сравнение значений

Когда обе стороны развёрнуты до обычных значений, остаётся сравнить. Ожидаешь однострочник a.Interface() == b.Interface(), но есть ловушка. Одно и то же логическое число может храниться разными типами. В DTO поле CityID int, а в модели базы CityID int64. Для оператора == в Go это разные типы, и сравнение их упакованных форм вернёт false.

Поле будет помечаться как изменённое при каждой синхронизации. Поэтому compareValues приводит числовые семейства к общему виду:
func (m *Mapper) compareValues(a, b reflect.Value) bool {
aKind := a.Kind()
bKind := b.Kind()

switch {
case m.isSignedInt(aKind) && m.isSignedInt(bKind):
return a.Int() == b.Int()
case m.isUnsignedInt(aKind) && m.isUnsignedInt(bKind):
return a.Uint() == b.Uint()
case m.isSignedInt(aKind) && m.isUnsignedInt(bKind):
return a.Int() >= 0 && uint64(a.Int()) == b.Uint()
case m.isUnsignedInt(aKind) && m.isSignedInt(bKind):
return b.Int() >= 0 && a.Uint() == uint64(b.Int())
case m.isFloat(aKind) && m.isFloat(bKind):
return a.Float() == b.Float()
default:
return a.Interface() == b.Interface()
}
}


Методы .Int(), .Uint() и .Float() расширяют любую разрядность до самой большой формы, так что int8, int16, int, int64 сворачиваются в одно сравнение. Случаи знаковое против беззнакового защищены проверкой >= 0, чтобы отрицательное число не превратилось в гигантское беззнаковое. Всё, что не число, падает в обычный ==.

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

Весь приём это смена вопроса. Вместо «как сравнить эти две конкретные структуры» автор спросил «как сравнить любые две структуры, которые сами рассказали, как их поля соотносятся». Теги уже несли эту информацию для GORM и JSON, осталось добавить один тег и научить маленькую функцию его читать.

Получилось около 130 строк рефлексии вместо растущей кучи хрупких блоков if. Код работает на всех моделях и становится проще в поддержке по мере роста системы. Если ловите себя на том, что пишете одну и ту же логику по полям в N-й раз, проверьте, не лежат ли метаданные для обобщения прямо в ваших тегах. Чаще, чем кажется, они там уже есть.

📍 Навигация: Вакансии • Задачи • Собесы

🐸 Библиотека Go-разработчика

#GoDeep
  • 👍 2
  • ❤ 1
More from @goproglib
  1. Sep 25, 2026🤡🤡 📍 Навигация: Вакансии • Задачи • Собесы 🐸 Библиотека Go-разработчика #GoGiggle
  2. Sep 25, 2026💡 Код работает. А data race уже есть В Go можно записать значение в одной горутине, прочи…
  3. Sep 23, 2026💥 TCP/IP: что происходит с данными в сети Когда Go-приложение отправляет данные по сети,…
  4. Sep 22, 2026🔴 Встроенные функции len, make, panic — встроенные функции Go, которые можно использовать…
  5. Sep 21, 2026🤩 Go с нуля: серия базовых шпаргалок Собрали путь от Hello, World! до goroutines, channel…
  6. Sep 20, 2026🤔 Вопрос с собеседования по Go Что выведет программа? ❤️ — [1 2 3 4 5] / [1 2 10] 🔥 — [1…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →