Go: SQL
Теория: Структуры данных и маппинг
После выполнения запросов программе нужно преобразовать строки результата в удобные структуры Go. Такой маппинг определяет, как колонки из таблиц превращаются в поля структуры, как обрабатываются NULL-значения и каким образом считывается время.
Когда приложение читает данные из базы, оно связывает строки результата со структурами Go через Scan(). В запросе перечисляют колонки в нужном порядке, а Scan() заполняет поля структуры в таком же порядке. Программа получает одну строку или поток строк, и каждую строку она считывает в заранее описанную структуру, где типы совпадают с типами в базе.
При чтении строк из базы все поля структуры получают значения из Scan(). Но не каждое поле в таблице гарантировано заполнено, поэтому программа должна уметь принимать NULL и отличать отсутствие данных от обычного значения.
Нулевые значения и типы sql.Null*
Когда в таблице есть колонка, которая может содержать NULL, у программы возникает простой вопрос: «А что я должен сюда читать, если данных нет?». Обычные типы Go — строки, числа, булевы значения — не умеют хранить отсутствие данных. Если попытаться сканировать NULL прямо в string или int, драйвер выдаст ошибку: он не знает, какое значение туда положить.
Чтобы такие ситуации обрабатывать безопасно, в пакете database/sql есть специальные контейнеры — sql.NullString, sql.NullInt64, sql.NullBool, sql.NullTime. Каждый такой тип хранит два поля: само значение и флаг Valid. Если колонка в базе действительно содержала данные, Valid будет true. Если пришёл NULL — значение остаётся пустым, а Valid становится false. Код может отличить: данных нет, или просто значение было пустым.
Но есть нюанс. В прикладном коде Go такие контейнеры используются всё реже. На практике почти всегда выбирают другой путь — сканируют значения в указатели на обычные типы. Если колонка не NULL, указатель получает значение. Если NULL — указатель остаётся nil. Такой подход проще, короче и органичнее выглядит в структуре.
Поэтому sql.Null* остаются скорее инструментом для особых случаев, когда нужны строгие правила работы с NULL. В обычной работе чаще используют обычные типы с указателями.
Помимо текстовых и числовых колонок в таблицах часто встречаются отметки времени. Драйвер считывает их в тип time.Time, но работа с датами требует учёта формата хранения и часовых поясов.
Работа со временем: time.Time и timestamp
Когда таблица содержит отметки времени, программа читает их в тип time.Time. База хранит TIMESTAMP или TIMESTAMPTZ, а драйвер превращает значение в структуру времени со смещением. Если колонка описана как TIMESTAMPTZ, в time.Time уже будет корректное смещение; если используется TIMESTAMP без зоны, драйвер интерпретирует его в локальном времени. При записи времени в базу программа может передавать time.Time напрямую, а при чтении выводить его в нужном формате через метод Format().


