Гайдлайны

Внутренние гайдлайны разработки

Общая концепция разработки сервиса

Общая архитектура

Сервис разрабатывается в слоеной модели. Код содержит три основных слоя в виде пакетов:

Отдельно стоит пакет domain, который:

Этот пакет используется всеми компонентами системы.

 

Обработка ошибок

Общие требования

Для работы с ошибками требуется использовать вместо стандартного пакета errors, пакет github.com/pkg/errors, так как он позволяет учитывать стэк вызова, при логировании ошибки.

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

Если соответствующей ошибки нет, ее можно добавить, согласовав назначение нового класса ошибок. Не следует делать специализированные классы ошибок, класс должен отражать широкое множество ошибок схожих по области в которой они возникли. Например, ValidationError - класс, отражающий любые ошибки валидации, не следует создавать MyStructFieldValidationError, дабы не плодить сущности.

Централизованная обработка ошибок

Условно ошибки можно разделить на два вида:

Сервис обрабатывает ошибки в едином обработчике, следующим образом:

При любом варианте записи в лог вызывающему отдается ответ c классом ошибки и причиной её возникновения. Ответ выглядит следующим образом:

{
  "success": false,
  "message": "Внутренняя ошибка сервиса",
  "error": {
    "class": "internal",
    "type": "runtime.errorString",
    "reason": "runtime error: integer divide by zero"
  }
}

Класс ошибки определяет важность для клиентского приложения, сообщение описывает проблему, объект error показывает причину возникновения ошибки.

Соответствие внутренней и внешней классификации ошибок:

  Класс ошибки apierror
handled Определяется разработчиком
unhandled internal
fiber internal

Опциональные значения в моделях

Цель

В некоторых случаях передача всех полей модели от клиента не имеет смысла. Например, клиент хочет обновить только наименование какой-то сущности. Необходимо предоставить простой программный интерфейс для объявления таких полей, получения значений только заполненных полей перед выполнением запроса к базе данных.

Пакет

gitlab.corp.rarus.cloud/rarus-lms/backend/pkg/optional

Пакет предоставляет обобщенный тип optional.Optional[T any], который оборачивает другой тип и хранит указатель на значение этого типа. Таким образом можно легко различить наличие и отсутствие значения в опциональном поле.

Например, имеем модель клиента, у которого есть необязательное для передачи поле age:

type Client struct {
	Id   uint64                   `json:"id"`
	Name string                   `json:"name"`
	Age  optional.Optional[uint8] `json:"age"`
}

При входном JSON отсутствующим полем age, поле не имеет значения:

jsonData := []byte(`{
        "id": 1,
        "name": "Василий",
    }`)
c := Client{}
json.Unmarshal(jsonData, &c) // Обертка использует парсер внутреннего типа

c.Age.IsSet() // если значение не установлено - false

v, err := c.Age.GetValue() //если значение == nil, возвращает error

c.Age.Value // указатель на внутренний тип, *uint8 в данном случае

v = c.Age.OrDefault(30) // если значение не установлено, вернет значение по-умолчанию
// v == 30, но c.Age.IsSet() == false

c.Age.SetValue(30) // Теперь значение изменилось и изменения будут учтены при получении map[string]any

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

jsonData := []byte(`{
"id": 1,
"name": "Василий",
}`)
c := Client{}
json.Unmarshal(jsonData, &c)

m := optional.StructToMap(c) // здесь будет map[string]any, но содержащая только обязательные поля и Optional поля с заполненными значениями
// m = {"id":1, "name":"Василимй"}

k,v := optional.GetKeysAndValues(m) // далее можно получить ключи и значения, для передачи в аргументы драйвера к базе
// k = ["id", "name"]
// v = [1, "Василий"]

squirrel.Update("some_table").Columns(k).Values(v).Where(...)

Функция optional.StructToMap формирует ключи хэш-таблицы по следующим правилам: