Проблема
В golang нет undefined/none
, из-за чего структуры, функции обычные и переменные нельзя использовать гибко - нет синтаксического сахара, как в python. Есть значение nil
, но оно тоже не дает понимания, было ли значение передано или нет, так как golang по умолчанию задает значения переменным или полям структуры, например:
дана структураtype Person struct {
Name string
Position string
}
person := Pesron{Name: "Robert"}
при получении поля выдается значение по умолчанию (поле Position)fmt.Println(person.Name) // Robert
fmt.Println(person.Position) // пустая строка
Детализация проблемы
Задаем два экземпляра структурыperson1 := Pesron{Position: "Junior"}
person2 := Pesron{}
результат
fmt.Println(person1.Position) // пустая строка
fmt.Println(person2.Position) // пустая строка
но поле Position не было ведь задано во втором случае!
Есть вариант сделать поле Position через pointertype Person struct {
Name string
Position *string
}
Тогда position1 := "Junior"
person1 := Pesron{Position: "Junior"}
position2 := ""
person2 := Pesron{Position: ""}
person3 := Pesron{}
fmt.Println(person1.Position) // Junior
fmt.Println(person2.Position) // ""
fmt.Println(person3.Position) // nil
Естественно, в данном случае уже есть различие, но есть ситуации, где nil может быть использован тоже как значение!
Где это критично
Дано:
пакеты pgx, squirell, uuid
Имеется таблица в БДCREATE TABLE persons(
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
position VARCHAR(255) NULL
);
Имеется dto
для получения списка []Person
type GetPersonListDTO struct {
Name string
Position *string
Limit int
Offset int
}
Метод репозитория для работы с БДfunc (r *Repository) GetPersonList(ctx context.Context, dto GetPersonListDTO) []Person {
query := sq.Select("id", "name", "position").From("persons").
Where(sq.And{sq.Eq{"name": dto.Name}}).
Where(sq.And{sq.Eq{"position": pgtype.Text{String: *dto.Position, Valid: true}}).
Limit(dto.Limit)
Offset(dto.Offset)
sqlString, , := query.ToSql()
// далее сделать запрос в БД
}
Рассмотрим случаи, где это критично
Случай 1: Передача опциональных параметров в запросdto := GetPersonListDTO{Name: "Robert"}
r.GetPersonList(ctx, dto)
sql запрос будет выглядеть следующим образомSELECT id, name, position FROM persons WHERE name = 'Robert' AND position = '' LIMIT 0 OFFSET 0
так как структура GetPersonListDTO
имеет значения по умолчаниюdto.Position // пустая строка
dto.Limit // 0
dto.Offset // 0
Но для получения результата был передан всего один аргумент - Name
. Как решить эту проблему?
Случай 2: Изменим функцию для обработки переданных аргументов и структуру GetPersonListDTO
type GetPersonListDTO struct {
Name *string
Position *string
Limit *int
Offset *int
}
func (r Repository) GetPersonList(ctx context.Context, dto GetPersonListDTO) []Person {
query := sq.Select("id", "name", "position").From("persons")
if dto.Name != nil {
query = query.Where(sq.And{sq.Eq{"name": dto.Name}})
}
if dto.Position != nil {
query = query.Where(sq.And{
sq.Eq{
"position": pgtype.Text{String: dto.Position, Valid: true}}})
}
if dto.Limit != nil {
query = query.Limit(*dto.Limit)
}
if dto.Offset != nil {
query = query.Offset(*dto.Offset)
}}
sqlString, , := query.ToSql()
// далее сделать запрос в БД
}
Передача аргументов в функциюdto := GetPersonListDTO{Name: "Robert"}
r.GetPersonList(ctx, dto)
sql запрос будет выглядеть следующим образомSELECT id, name, position FROM persons WHERE name = 'Robert'
В данном случае проблема с опциональной передачей аргументов на первый взгляд решена.
Но поле position
в БД имеет NULL
значение. Вопрос: как отфильтровать по условию WHERE position = null
?
Проверка значения dto.Position != nil
уже не работает, так как если передавать в функцию dto := GetPersonListDTO{Name: "Robert"},
то значениеdto.Position
будет равно по умолчанию nil
.
То есть в данном случае нет возможности никак проверить, заполнено поле в dto
или нет, даже nil
значением, и соответственно, нет возможности получить из БД данные, отфильтрованные по nil
.
Из вышесказанного, имеем:
1. Есть заполнение по умолчанию
2. Есть возможность обойтись через nil
в некоторых случаях
3. Nil
накладывает ограничения на использование опциональности
То есть golang не дает полноценный функционал для опциональной работы с полями из-за особенностей языка.
Решение проблемы
Проблему можно решить путем создания нового типа, который является оберткой над стандартными и пользовательскими типами. type OptionalType[T any] struct {
Value T
Defined bool
Valid bool
}
Из чего состоит этот типValue - значение
Valid - флаг, является ли переданное значение nil
Defined - флаг, который показывает, было ли поле определено или нет
Функция, с помощью которой можно создать опциональный типfunc NewOptionalType[T any](v T) OptionalType[T] {
if v == nil {
return OptionalType[*T]{nil, true, false}
}
return OptionalType[*T]{v, true, true}
}
Этот тип можно заставить работать как стандартный тип golang. Для этого мною был написан пакет github.com/denpa16/optional-go-type
. Ниже - как работать с ней.
Библиотека optional-go-type
Заполнение структур
Библиотека уже имеет опциональные типы, построенные на встроенных типах golang
например:import optionalType "github.com/denpa16/optional-go-type"
type Person struct {
Name optionalType.OptionalString
Position optionalType.OptionalString
Age optionalType.OptionalInt
}
создание экземпляра структурыimport optionalType "github.com/denpa16/optional-go-type"
name := "Robert"
position := "Junior"
age := 20
person1 := Person{ Name: optionalType.NewOptionalString(&name)
Position: optionalType.NewOptionalString(&name)
Age: optionalType.NewOptionalInt(&age)
}
person2 := Person{
Name: optionalType.NewOptionalString(&name)
Position: optionalType.NewOptionalString(nil)
Age: optionalType.NewOptionalInt(nil)
}
person3 := Person{
Name: optionalType.NewOptionalString(&name)
}
Как отрабатывают опциональные поля
В первом случаеfmt.Println(person1.Position.Value) // Junior
fmt.Println(person1.Position.Valid) // true
fmt.Println(person1.Position.Defined) // true
person1.Position.Valid = true
, так как значение не равно nilperson1.Position.Defined = true
, так как значение было определено
Во втором случае
fmt.Println(person2.Position.Value) // Junior
fmt.Println(person2.Position.Valid) // true
fmt.Println(person2.Position.Defined) // true
person2.Position.Valid = false
, так как значение равно nilperson2.Position.Defined = true
, так как значение было определено
В третьем случае
fmt.Println(person2.Position.Value) // пустая строка
fmt.Println(person2.Position.Valid) // false
fmt.Println(person2.Position.Defined) // false
person2.Position.Valid = false
, так как значение равно nilperson2.Position.Defined = false
, так как значение НЕ было определено
Маршаллинг и анмаршаллинг
type Person struct {
Name optionalType.OptionalString `json:"name"`
Position optionalType.OptionalString `json:"position"`
Age optionalType.OptionalInt `json:"age"`
}
json.Unmarshall
jsonData := []byte({"Name": "Robert"})
person := Person{}
_ = json.Unmarshal(jsonData, &person)
person.Name.Value // Robert
person.Name.Defined // true
person.Name.Valid // true
person.Position.Value // nil
- так как в json не было передано это полеperson.Position.Defined // false
- так как в json не было передано это полеperson.Position.Valid // false
- так как в json не было передано это поле
json.Marshallname := "Robert"
person := Person{
Name: optionalType.NewOptionalString(&name)
}
result, _ := json.Marshal(person)
// Output: {"Name":"Robert"}
Маршаллинг и анмаршаллинг работают как обычно со всеми своими тэгами
Как использовать в критичных местах
Как выглядит метод репозитория (из случая 1 и случая 2 выше) с использованием OptionalType
type GetPersonListDTO struct {
Name OptionalString
Position OptionalString
Limit OptionalInt
Offset OptionalInt
}
func (r Repository) GetPersonList(ctx context.Context, dto GetPersonListDTO) []Person {
query := sq.Select("id", "name", "position").From("persons")
if dto.Name.Defined != nil {
query = query.Where(sq.And{sq.Eq{"name": dto.Name.Value}})
}
if dto.Position.Defined != nil {
query = query.Where(sq.And{
sq.Eq{
"position": pgtype.Text{String: dto.Position.Value, Valid: true}}})
}
if dto.Limit.Defined != nil {
query = query.Limit(*dto.Limit.Value)
}
if dto.Offset.Defined != nil {
query = query.Offset(*dto.Offset.Value)
}}
sqlString, , := query.ToSql()
// далее сделать запрос в БД
}
Передача аргументов в функцию
1. Передача части параметровdto := GetPersonListDTO{Name: "Robert"}
r.GetPersonList(ctx, dto)
sql запрос будет выглядеть следующим образом
SELECT id, name, position FROM persons WHERE name = 'Robert'
2. Передача части параметров с nil
для фильтрации по NULL
name := "Robert"
dto := GetPersonListDTO{Name: &name, Position: nil}
r.GetPersonList(ctx, dto)
sql запрос будет выглядеть следующим образом
SELECT id, name, position FROM persons WHERE name = 'Robert' AND position = NULL
3. Передача части Nullable
параметров со значением
name := "Robert"
position := "Junior"
dto := GetPersonListDTO{Name: &name, Position: &position}
r.GetPersonList(ctx, dto)
sql запрос будет выглядеть следующим образом
SELECT id, name, position FROM persons WHERE name = 'Robert' AND position = 'Junior'
Таким образом, есть возможность:
1. Не фильтровать вообще, если параметр не был передан для формирования запроса
2. Формировать запрос с NULL
значением
3. Формировать запрос с переданным значением в NULLABLE поле
OptionalType позволяет гибко работать со структурами и функциями, где нельзя обойтись использованием только nil
и значением по умолчанию
Исходный код пакета
Как добавить пакет в свой go проектgo get github.com/denpa16/optional-go-type