В прошлой части мы дошли до момента, где надо сравнить два поля. На этом месте чаще всего ломаются наивные реализации. Реальные модели базы полны
*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