Проблема
В golang нет undefined/none, из-за чего структуры, функции обычные и переменные нельзя использовать гибко - нет синтаксического сахара, как в python. Есть значение nil, но оно тоже не дает понимания, было ли значение передано или нет, так как golang по умолчанию задает значения переменным или полям структуры, например:
дана структураtype Person struct { Name string Position string } person := Pesron{Name: "Robert"}
при получении поля выдается значение по умолчанию (поле Position)fmt.Println(person.Name) // Robertfmt.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) // Juniorfmt.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 - флаг, является ли переданное значение nilDefined - флаг, который показывает, было ли поле определено или нет
Функция, с помощью которой можно создать опциональный тип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) // Juniorfmt.Println(person1.Position.Valid) // truefmt.Println(person1.Position.Defined) // trueperson1.Position.Valid = true, так как значение не равно nilperson1.Position.Defined = true, так как значение было определено
Во втором случае
fmt.Println(person2.Position.Value) // Juniorfmt.Println(person2.Position.Valid) // truefmt.Println(person2.Position.Defined) // true
person2.Position.Valid = false, так как значение равно nilperson2.Position.Defined = true, так как значение было определено
В третьем случае
fmt.Println(person2.Position.Value) // пустая строкаfmt.Println(person2.Position.Valid) // falsefmt.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 // Robertperson.Name.Defined // trueperson.Name.Valid // trueperson.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
Комментарии (19)

ertaquo
05.08.2025 10:23https://github.com/samber/mo вам в помощь. Буквально то же самое - опции, результаты и действия над ними. Заодно и https://github.com/samber/lo - кучка удобных функций :)

denpavlov16 Автор
05.08.2025 10:23Спасибо за наводку!
Будем исследовать и изучать как можно внедрить в свои проекты!

uvelichitel
05.08.2025 10:23Можно использовать конструкторы
func NewPerson() *Person
и точно знать во что поля установлены по дефолту.
В вашем подходе можно поэкономить память. Типboolв Go это aliasint8. То есть вам не нужно две переменныхboolдля флагов. Одной int8 хватит на восемь бинарных флагов с применением битовых масок. Так сделано например в стандартном пакетеosдля масок атрибутов файла https://pkg.go.dev/os#pkg-constants Идея простаconst Defined int8 = 1
const Valid int8 = 2
var Mark int8
Mark += Valid
if Mark&Defined != 0 {//false}
if Mark&Valid != 0 {//true}
denpavlov16 Автор
05.08.2025 10:23Спасибо за комментарий!
Да, это красиво
Но писать конструктор для каждой структуры не всегда хочется
+ при конструкторе nil всегда будет
а как понять, nil был прокинут ранее или по дефолту задан?
Хочется просто работать как с обычными типами, у которых просто внутри заложен этот механизм
Насчет оптимизации памяти - постараюсь доработать в следующих версиях

qrKot
05.08.2025 10:23Вот тут по коду чуть-чуть рекомендаций себе позволю, если вы не против.
const Defined int8 = 1В таких конструкциях лучше таки беззнаковые типы использовать.
int8 заполненный единицами в бинарном представлении - это -128 (выглядит странно, да?), а uint8 с тем же содержимым - 255.
С uint'ом открывается путь к грязным фокусам со сравнением. Например `mark > 7` вернет true, если есть хоть 1 взведенный флаг кроме первых трех. С int8 сравнение сфейлится, если будет взведен 8-й.
Mark += Valid
Вот тут однозначно Mark = Mark|Valid.
mark := 4 [00000100] fmt.Println(mark + 5) // 9 [00001001] fmt.Println(mark|5) // 5 [00000101]Вы вместо того, чтобы флаг взвести, суммируете числа. Чревато побочками, если в исходном значении не 0.
type bitmap uint8 const ( defined bitmap = 1 << iota // 1 [00000001] valid // 2 [00000010] accepted // 4 [00000100] // дальше само инкрементить поразрядно будет ) var mark = 5 // [00000101] - взведены 1 и 3 флаги // Простые сравнения на наличие одного флага. // В этом кейсе следующие 3 строки семантически идентичны fmt.Println( mark & defined != 0 ) // true fmt.Println( mark & defined > 0 ) // true fmt.Println( mark & defined == defined ) // true // И эти 3 сравнивают одно и то же fmt.Println( mark & valid != 0 ) // false fmt.Println( mark & valid > 0 ) // false fmt.Println( mark & valid == valid ) // false // Гораздо интереснее сравнение с несколькими флагами // Проверяем, что все указанные флаги взведены - `mark & flags == flags` fmt.Println(mark & (defined|valid) == defined|valid) // true, т.к. ОБА флага взведены fmt.Println(mark & (defined|accepted) == defined|accepted) // false, т.к. взведен только ОДИН флаг.

vvardenfell
05.08.2025 10:23А почему здесь "пустая строка" в person1.Position? Вы же инициализировали "Junior". Ну и опечатки

Здесь код не скомпилируется даже, переменные position1 и position2 не используются:


qrKot
05.08.2025 10:23type Person struct { Name string Position *string } position1 := "Junior" person1 := Pesron{Position: "Junior"} // ТУТ НЕ СКОМПИЛИРУЕТСЯ position2 := "" person2 := Pesron{Position: ""} // И ТУТ ТОЖЕ person3 := Pesron{}Ну блин, ну код-то свой запустить можно попробовать разок?
Ну и "библиотеку" посмотрел:
OptionalInt = OptionalType[*int] OptionalBool = OptionalType[*bool]Кхм... bool-значение, занимающее 16 байт в стеке и еще 8 в куче. А может, ну его, этот ваш паттерн Optional (ну либо уже просто возьмите существующие реализации, и не парьте мозг). Ну и да, раздельные поля defined/valid нафиг не нужны (ну либо вы мне сейчас расскажете, зачем инициализировать переменную невалидным значением).

denpavlov16 Автор
05.08.2025 10:23Спасибо за комментарий! Ошибки есть, не спорю
Но Вы поругаться пришли?) Или так, свое мнение оставить? Я Вас не заставляю качать библиотеку и использовать
Вроде как Хабр свободная площадка
Лучше бы предложили свое решение проблемы касаемо запросов в БД - как делать универсальную функцию, которая будет в зависимости от заполненности/незаполненности параметров формировать sql запрос
Или как Вы узнаете, поле было заполнено nil-ом или нет?
Ощущение, что Вы на go пишете просто посредственные вещи без каких-то паттернов, DRY, SOLID и прочее
qrKot
05.08.2025 10:23Лучше бы предложили свое решение проблемы касаемо запросов в БД
sqlc
Или как Вы узнаете, поле было заполнено nil-ом или нет?
Зачем?
Ощущение, что Вы на go пишете просто посредственные вещи без каких-то паттернов, DRY, SOLID и прочее
Ну вот про SOLID: буковка S говорит о том, что в реализации паттерна Optional одно из полей valid/defined - лишнее.
type Optional[T any] struct { Value T IsSet bool }Все, готовый Optional. Всяческие OptionalBool и OptionalInt - не нужны, тем более в том виде, который вы использовали.
OptionalInt = OptionalType[*int]Вот это вы зачем написали? Зачем тут указатель на int? Из принципа "что-то у меня оперативки многовато" или "а давайте посмотрим, насколько говенный у нас сборщик мусора"?
Просто вопрос: сколько памяти и где занимает ваш OptionalInt?

denpavlov16 Автор
05.08.2025 10:23Готовые решения - знал бы я о них раньше - не писал бы свою либу
"ну либо вы мне сейчас расскажете, зачем инициализировать переменную невалидным значением" - ощущение, что Вы не пишете продакшн код
qrKot
05.08.2025 10:23отовые решения - знал бы я о них раньше - не писал бы свою либу
И эти люди...
"ну либо вы мне сейчас расскажете, зачем инициализировать переменную невалидным значением" - ощущение, что Вы не пишете продакшн код
Ну т.е. не расскажете...

imasdf
05.08.2025 10:23я правильно понимаю, что данная библиотека решает проблему составления SQL запроса с помощью ORM, когда ORM является антипаттерном?
40kTons
Переизобрели шарповский Nullable на гошный манер?
denpavlov16 Автор
Может быть, сам не силен в шарпах
Библиотека создавалась из потребности решить свои проблемы неопределенности, а также узнать как эту проблему обходят другие
Постарался сделать удобно и в общем виде, может кто еще будет пользоваться
wmns
Эту проблему другие не обходят, потому что это не проблема.
denpavlov16 Автор
Может быть у других просто нет таких кейсов
Либо они делают костыли
Либо у них есть команда инфраструктуры, которая делает свои либы
как и писал выше, хотел узнать как делают другие