---
title: "Чтение записей: First, Find — Take, Last, сканирование и ловушка chaining"
description: "Поиск gorm first find: db.First по первичному ключу, Take, Last, срез через db.Find, проекции select gorm, Pluck, Count, ErrRecordNotFound и ловушка chaining."
course: "GORM: ORM-библиотека для Go"
url: "https://golangmaster.ru/courses/gorm/lessons/06-read/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Чтение записей: First, Find — Take, Last, сканирование и ловушка chaining

*Курс «GORM: ORM-библиотека для Go», урок 6 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/gorm/lessons/06-read/*

Запрос gorm first find приводит сюда тех, кому нужно вытащить записи из базы: db.First по первичному ключу, db.Take без сортировки, db.Last в обратную сторону и срез через db.Find. Пятый урок положил пять задач в todo_items — теперь их надо прочитать. Разберём ErrRecordNotFound и идиому errors.Is, первые условия Where, сканирование в структуры и проекции select gorm, Pluck и Count — и ту самую ловушку chaining, о которой предупреждал пятый урок: на чтении она стреляет больнее всего.

Пятый урок [заполнил таблицу](/courses/gorm/lessons/05-create/) пятью задачами — теперь на них нужны запросы. Со стороны базы чтение — это SELECT, и по частоте он обгоняет всё остальное вместе взятое: на одну вставку в живом сервисе приходятся десятки выборок. В стандартной библиотеке каждый SELECT собирается руками: строка запроса, плейсхолдеры, rows.Next, ручное перекладывание колонок в поля структуры. GORM раздаёт эту работу финишерам: First, Take и Last достают одну запись — с LIMIT 1 и разными сортировками, Find — срез записей без ограничений, Pluck и Count — колонку и число. Одна честная оговорка о формате: СУБД в браузерной песочнице не живёт, поэтому runner-ов в курсе нет — вместо запуска вас проверяют quiz, predict, fill, order и карточки, а примеры рассчитаны на локальный Go с PostgreSQL или SQLite. Схема и данные — из четвёртого и пятого уроков; если делали домашку, таблица уже заполнена.

### gorm first find: три финишера одной записи

Начнём с внутренностей: в finisher_api.go библиотеки все три финишера одиночной записи устроены одинаково и отличаются буквально одной строкой. Каждый сначала ограничивает выборку — Limit(1), то есть LIMIT 1 в SQL: базе достаточно вернуть одну строку, лишнее сканирование отсечено. First и Last добавляют сортировку по первичному ключу — клоз ORDER BY с колонкой ключа: у First по возрастанию, у Last по убыванию (Desc: true в том же clause.OrderByColumn). Take сортировку не добавляет вообще — вернётся то, что база отдаст первой. Дальше все трое включают флаг RaiseErrorOnNotFound: если запрос не вернул ни строки, финишер вернёт ErrRecordNotFound, а не молчаливый ноль в RowsAffected. Find устроен скромнее: ни LIMIT, ни ORDER BY, ни флага ошибки — он выполняет условия и раскладывает результат в то, что вы передали. Отсюда правило выбора: одна запись с гарантированным порядком — First или Last, одна запись «как попало» или со своей сортировкой — Take, всё остальное — Find.

### Первое чтение и SQL-след

```go
var first, taken, last TodoItem

result := db.First(&first) // ORDER BY "todo_items"."id" LIMIT 1
db.Take(&taken)            // без ORDER BY: что база отдаст первой
db.Last(&last)             // ORDER BY "todo_items"."id" DESC LIMIT 1

fmt.Println(first.Title, last.Title)
// Купить хлеб Обновить бюджет: ключи 1 и 5 из пятого урока

if errors.Is(result.Error, gorm.ErrRecordNotFound) {
        log.Println("таблица пуста")
}

// SQL-след (PostgreSQL):
// SELECT * FROM "todo_items" ORDER BY "todo_items"."id" LIMIT 1
// SELECT * FROM "todo_items" LIMIT 1
// SELECT * FROM "todo_items" ORDER BY "todo_items"."id" DESC LIMIT 1
```

В таблице пять задач с ключами 1-5, поэтому First приносит «Купить хлеб», Last — «Обновить бюджет». Вызов Take без своего Order в комментариях мы даже не берёмся расшифровывать: недетерминированный он. Обратите внимание на приёмник: &first — указатель на нулевую структуру, GORM сам выделит и заполнит поля; проверка результата — через result.Error, как у Create.

### db.First по первичному ключу: inline-условия

Самый частый шаблон чтения — достать запись по ключу. Условие передаётся прямо в финишер, это inline-условия: db.First(&task, 3) построит WHERE id = 3 и добавит свой LIMIT 1 с сортировкой. Число можно отдать строкой — db.First(&task, "3") даст тот же запрос: аргумент приводится к ключу. Для строковых ключей вроде uuid форма другая — db.First(&task, "id = ?", "uuid-строка"): с плейсхолдером, как положено SQL. Срез ключей разворачивается в IN: db.Find(&todos, []int{1, 2, 3}) уедет с WHERE id IN (1,2,3). Есть и скрытая форма: если в переданной структуре ключ уже заполнен, он сам станет условием — var draft = TodoItem{ID: 10}; db.First(&draft) выполнит WHERE id = 10. Нюанс напоследок: сортировку по ключу First и Last знают, только когда им передали указатель на структуру или вызвали через db.Model(&TodoItem{}); на чужой приёмник вроде db.Table("todo_items").First(&map) связка не работает — документация прямо предупреждает, — а Take в той же ситуации работает, сортировать он и не собирается.

### Что вернёт First на пустой таблице?

```go
var t TodoItem
err := db.First(&t).Error
fmt.Println(err, t.ID, t.Title)
```

**Что выведет программа?**

- ✅ record not found 0  — ошибка not found, структура осталась нулевой
- <nil> 0  — ошибки нет, просто ничего не прочитано
- <nil> 1 Купить хлеб — GORM сам подставил первую запись
- паника: нельзя читать в нулевую структуру

Флаг RaiseErrorOnNotFound включён, таблица пуста — First вернёт gorm.ErrRecordNotFound, который fmt печатает текстом «record not found». Структура при этом остаётся нулевой: ID 0, Title пустая строка. Проверяйте err до использования полей — именно поэтому одиночное чтение всегда идёт в паре с проверкой.

### ErrRecordNotFound и идиома errors.Is

ErrRecordNotFound — главная ошибка чтения, и проверять её принято канонически: errors.Is(result.Error, gorm.ErrRecordNotFound). Не через ==: это та же идиома работы с ошибками, что разобрана в курсе go-basics на уроке про несколько возвращаемых значений и ошибки — errors.Is разворачивает цепочку обёрток и находит ошибку, даже если между вами и оригиналом встал fmt.Errorf с %w или код диалекта. Прямое сравнение увидит только точный оригинал. Что делать дальше — два пути. Если «записи нет» — штатный сценарий (покажем пустой экран, отдадим 404, создадим черновик), ловите ошибку и ветвитесь. Если запись обязана существовать, а не-found — ЧП, берите альтернативу из документации: db.Limit(1).Find(&task) — Find флаг RaiseErrorOnNotFound не включает, ошибки not found не будет, проверяйте result.RowsAffected == 0. Только не пишите db.Find(&task) без Limit: такой запрос просканирует всю таблицу и вернёт первую попавшуюся строку — недетерминированно, без LIMIT 1, зато с лишней работой для базы.

### Проверка: Take на пустой таблице

**Вопрос:** Таблица todo_items пуста. Выполнили var t TodoItem; err := db.Take(&t).Error. Чему равен err?

- nil: Take не проверяет not found
- ✅ gorm.ErrRecordNotFound: у Take включён тот же флаг RaiseErrorOnNotFound, что у First и Last
- gorm.ErrEmptySlice: чтение в нулевую структуру запрещено
- Панику: нельзя сканировать в незаполненную переменную

Take отличается от First и Last только отсутствием сортировки — механизм ошибки общий: флаг RaiseErrorOnNotFound включён, пустой результат вернёт ErrRecordNotFound. Указатель на нулевую структуру — законный приёмник: поля заполнятся при сканировании. ErrEmptySlice — ошибка вставки из пятого урока, к чтению она отношения не имеет.

### First, Take, Last, Find: сравнение

| Финишер | ORDER BY | LIMIT | Пустой результат |
| --- | --- | --- | --- |
| db.First(&t) | первичный ключ по возрастанию | 1 | ErrRecordNotFound |
| db.Take(&t) | нет — как вернёт база | 1 | ErrRecordNotFound |
| db.Last(&t) | первичный ключ по убыванию | 1 | ErrRecordNotFound |
| db.Limit(1).Find(&t) | нет, если не задали | 1 | nil: RowsAffected 0 |
| db.Find(&t) в структуру | нет | нет | nil: первая попавшаяся строка |
| db.Find(&todos) в срез | нет | нет | nil: пустой срез, RowsAffected 0 |

### Take: сортировка ваша, а не базы

Зачем Take, если First предсказуемее? Затем, что сортировка по ключу — не единственный порядок в жизни. Когда порядок диктуете вы, внутренний ORDER BY только мешает: db.Order("priority DESC, created_at ASC").Take(&task) возьмёт самую приоритетную задачу, а не самую старую по id. Take же незаменим там, где модели нет: выборки из чужой таблицы через db.Table, приёмники-карты — GORM не знает схемы и не может угадать ключ, а Take и не пытается. Обратная сторона честности: без явного Order результат Take недетерминирован — «первая строка» зависит от плана запроса, физического порядка строк в таблице и версии оптимизатора. В тестах такое оставлять нельзя: либо Order, либо First. Практическое правило звучит так: Take — «мне нужна одна строка из этого набора; порядок описал я сам, или он не важен». Всё остальное — территория First и Last.

### db.Find: срез задач

```go
var todos []TodoItem

result := db.Find(&todos) // SELECT * FROM "todo_items"
fmt.Println(len(todos), result.RowsAffected) // 5 5
fmt.Println(result.Error)                    // <nil>

// Условия можно передать прямо в Find - inline:
var open []TodoItem
db.Find(&open, "done = ?", false)
// SELECT * FROM "todo_items" WHERE done = false

// Срез ключей разворачивается в IN:
var picked []TodoItem
db.Find(&picked, []int{2, 4})
// SELECT * FROM "todo_items" WHERE id IN (2,4)

// Пустая выборка - не ошибка:
var nothing []TodoItem
res := db.Find(&nothing, "done = ?", true)
fmt.Println(len(nothing), res.RowsAffected, res.Error) // 0 0 <nil>
```

Find — финишер наборов: срез не нужно инициализировать, GORM выделит память и допишет длину. RowsAffected совпадает с len среза — числом прочитанных строк. Главное отличие от одиночных финишеров: ErrRecordNotFound здесь не существует, пустая выборка — штатный nil-ответ с нулём в RowsAffected. Фильтровать можно и строкой-условием inline, и цепочкой Where — разница чисто стилистическая. Огромный набор, не влезающий в память целиком, читают родственником FindInBatches: порции по N записей с колбэком — вернёмся к нему, когда дойдём до больших выборок.

### Сколько строк вернёт Find с условием?

```go
// В таблице 5 задач; две из них уже done = true
var closed []TodoItem
result := db.Find(&closed, "done = ?", true)
fmt.Println(len(closed), result.RowsAffected, result.Error)
```

**Что выведет программа?**

- ✅ 2 2 <nil>
- 0 0 <nil>
- 2 2 record not found
- 5 5 <nil>

WHERE done = true отфильтровал две задачи из пяти: len и RowsAffected равны двум. ErrRecordNotFound у Find не бывает — даже ноль строк это nil-ошибка и пустой срез. Если ответ «5 5» казался убедительным, перечитайте про inline-условия: условие уехало в WHERE, а не проигнорировалось.

### Условия на минимум: Where и нулевые поля

db.Find(&open, "done = ?", false) — короткий путь; постоянная форма — цепочка Where: db.Where("done = ?", false).Find(&todos). В строковом условии плейсхолдер ? подставляется безопасно: параметры едут отдельно от текста запроса, тот же механизм, что и в database/sql. Вторая форма — структурная: db.Where(&TodoItem{Status: "todo"}).Find(&todos) соберёт WHERE status = 'todo' из непустых полей структуры. Здесь притаилась ловушка, зеркальная zero values из пятого урока: нулевые поля в структурном условии игнорируются — Where(&TodoItem{Priority: 0}) не даст WHERE priority = 0, условие окажется пустым, и Find молча вернёт всю таблицу. Карты вместо структур нули включают: db.Where(map[string]interface{}{"priority": 0}).Find(&todos) даст ровно priority = 0. Выборка вернула «слишком много»? Проверьте, не схлопнулись ли нули. Полный арсенал — IN, LIKE, диапазоны, Or, Not, группировки — ждёт в седьмом уроке про Where и операторы; здесь нужна была рамка для чтения, не больше.

### Проверка: структурное условие с нулём

**Вопрос:** db.Where(&TodoItem{Priority: 0}).Find(&todos) — что попадёт в WHERE?

- priority = 0
- ✅ Условий не будет: ноль считается незаданным значением, в условие не едет — вернутся все строки
- Ошибка: пустое условие запрещено
- priority IS NULL

Структурные условия собираются только из ненулевых полей — ноль неотличим от «не заполнил». Явный ноль дают картой, строкой с плейсхолдером или вторым аргументом Where, перечисляющим поля: db.Where(&TodoItem{}, "Priority") включит ноль принудительно. Почему так — и как это связано с default-тегами, разберём в седьмом уроке про условия.

### Сканирование: структуры, проекции select gorm, карты

Как результат запроса попадает в ваши переменные? Сканирование идёт по конвенции из третьего урока: колонка snake_case соответствует полю CamelCase (title → Title), лишние колонки игнорируются, отсутствующие остаются нулями. Для полных моделей это db.First и db.Find из начала урока. Когда нужны только часть полей, работают проекции — те, кто ищет по запросу select gorm, имеют в виду ровно это: ограничить выборку колонками через Select и принять её лёгкой структурой-проекцией, приём Smart Select Fields из документации. Экономится трафик и память: тянуть Status, Priority и обе метки ради списка заголовков расточительно. Третий приёмник — карта: db.Model(&TodoItem{}).First(&row) с row map[string]interface{}{} разложит первую строку по именам колонок; для многих строк — срез карт. Model(&TodoItem{}) здесь обязателен: GORM должен знать, откуда читать таблицу. В карте лежат типы драйвера database/sql: числа int64, строки []byte, время time.Time — приводить придётся самим. Для динамики карта незаменима; в обычном коде я держусь структур — типы проверяет компилятор.

### Проекция в структуру и в карту

```go
// Проекция: только id и title, срез лёгких структур
type TodoCard struct {
        ID    uint
        Title string
}

var cards []TodoCard
db.Model(&TodoItem{}).Select("id", "title").Scan(&cards)
// SELECT "id", "title" FROM "todo_items"

// Одна строка в карту: имена колонок становятся ключами
row := map[string]interface{}{}
db.Model(&TodoItem{}).First(&row)
// SELECT * FROM "todo_items" ORDER BY "todo_items"."id" LIMIT 1

fmt.Println(row["title"]) // Купить хлеб

// Много строк - срез карт
var rows []map[string]interface{}
db.Model(&TodoItem{}).Find(&rows)
```

Scan — универсальный финишер сканирования: структура, срез структур, карта — приёмник может не совпадать с моделью, лишь бы имена полей совпали с колонками. Проекция TodoCard весит на порядки меньше полной записи — для списков и API-ответов это стандартный приём. С картой First работает только под Model: без модели GORM не знает таблицу — а вот сортировку по ключу при Model он добавит, как в SQL-следе.

### Pluck и Count: колонка и число

Два финишера для частичных результатов. Pluck вынимает одну колонку в срез: db.Model(&TodoItem{}).Pluck("id", &ids) положит в ids все ключи. Требование жёсткое: приёмник — срез, обычно примитивного типа; колонка — одна, не две. Нужны две колонки — это уже проекция: Select плюс Scan или Find, как в прошлом разделе. Имя GORM сначала ищет среди полей модели — Pluck("ID") поймёт, что речь о колонке id, — и Distinct с ним дружит: db.Model(&TodoItem{}).Distinct().Pluck("status", &statuses) даст SELECT DISTINCT "status", список уникальных статусов. Count считает строки: приёмник — указатель на int64, db.Model(&TodoItem{}).Count(&cnt) выполнит SELECT count(*). Условия применяются как обычно: Where перед Count отфильтрует. Тонкость изнутри: count(*) ставится только если вы не задали свой Select, а после выполнения прежний клоз SELECT восстанавливается — цепочка продолжается без сюрпризов. И не считайте строки через len(среза): тянуть все записи ради числа — работа для базы, а не для вашего кода.

### Pluck и Count на практике

```go
var ids []uint
db.Model(&TodoItem{}).Pluck("id", &ids) // [1 2 3 4 5]

var statuses []string
db.Model(&TodoItem{}).Distinct().Pluck("status", &statuses)
// SELECT DISTINCT "status" FROM "todo_items"

var cnt int64
db.Model(&TodoItem{}).Count(&cnt) // 5
db.Model(&TodoItem{}).Where("done = ?", false).Count(&cnt)
// SELECT count(*) FROM "todo_items" WHERE done = false

// Несколько колонок - уже не Pluck, а проекция:
var pairs []TodoCard
db.Model(&TodoItem{}).Select("id", "title").Scan(&pairs)
```

Pluck и Count — самые дешёвые запросы в этом уроке: одна колонка или одно число вместо всей таблицы. Оба признают Distinct, оба уважают Where. Пара Pluck("id") с последующим Find по списку ключей — типовой каркас «сперва узнали, что читать»; его обобщение с пагинацией ждёт вас в уроках про выборки по страницам.

### Шпаргалка: каким финишером читать

- Запись по ключу или «самая старая/новая» — db.First / db.Last: LIMIT 1 и сортировка по первичному ключу.
- Одна строка со своей сортировкой или из чужой таблицы — db.Order(...).Take.
- Список записей, возможно пустой, — db.Find в срез: пустая выборка не ошибка.
- Одна строка без ошибки not found — db.Limit(1).Find в структуру и проверка RowsAffected.
- Одна колонка списком — Pluck в срез; количество строк — Count(&int64).
- Произвольный SELECT — Scan в структуру-проекцию или в map[string]interface{}.

> **Ловушка: условия прилипают к инстансу.** Пятый урок обещал раскрыть её на чтении — вот документированный сценарий. queryDB := db.Where("done = ?", false) — и переменная поехала на два запроса. Первый, queryDB.Where("priority > ?", 2).Find(&a), отработает честно: WHERE done = false AND priority > 2. Второй, queryDB.Where("priority > ?", 5).Find(&b), тихо уедет с тремя условиями: WHERE done = false AND priority > 2 AND priority > 5 — старое условие осталось на инстансе и склеилось с новым. Если queryDB живёт в поле структуры или замыкании, весь сервис читает мусор, и никакая ошибка об этом не скажет. Лечение из документации: queryDB := db.Where("done = ?", false).Session(&gorm.Session{}) — сессия возвращает инстанс, безопасный для переиспользования: каждая новая цепочка стартует с чистым Statement. Либо дисциплина: цепочка начинается прямо с db и в переменные не сохраняется.

### Проверка: сколько условий во втором запросе?

**Вопрос:** queryDB := db.Where("done = ?", false). Затем дважды: queryDB.Where("priority > ?", 2).Find(&a) и queryDB.Where("priority > ?", 5).Find(&b). Что окажется в WHERE второго запроса?

- done = false AND priority > 5
- ✅ done = false AND priority > 2 AND priority > 5: оба старых условия прилипли к инстансу
- priority > 5
- Ошибка: повторное использование инстанса запрещено

Инстанс после цепочки хранит уже добавленные условия: каждое использование дописывает в тот же Statement. Второй запрос получил done = false из первого сохранённого Where, priority > 2 из первого запроса и своё priority > 5. Без Session(&gorm.Session{}) хранить такой инстанс в переменной нельзя — это и есть chaining-ловушка.

### Заполните пропуски: чтение и подсчёт

Восстановите чтение по ключу с канонической проверкой ошибки и подсчёт строк.

```go
var task TodoItem
err := db.___(&task, 7).Error // чтение по первичному ключу
if errors.___(err, gorm.___) {
        log.Println("задачи нет - покажем пустой экран")
}

var total ___
db.Model(&TodoItem{}).___(&total) // сколько записей в таблице
```

Подсказка: Финишер одиночной записи по ключу — First; идиома проверки — errors.Is(err, gorm.ErrRecordNotFound); Count принимает указатель на int64.

Правильные ответы в пропусках: `First`, `Is`, `ErrRecordNotFound`, `int64`, `Count`

### Соберите путь запроса db.First

Расставьте шаги от вызова финишера до возврата результата — в том порядке, в котором их делает GORM.

Правильный порядок строк:

1. `Вызван db.First(&task, 3): финишер чтения`
2. `В Statement добавлен клоз LIMIT 1`
3. `В Statement добавлен клоз ORDER BY по первичному ключу`
4. `Inline-условие 3 превращено в WHERE id = 3`
5. `СУБД выполнила SELECT, строка отсканирована в структуру`
6. `Пустой результат при RaiseErrorOnNotFound вернул ErrRecordNotFound`

### Карточки: чтение записей

- **db.First(&t, id)** — Одна запись по первичному ключу: LIMIT 1 и ORDER BY id. Пустой результат — ErrRecordNotFound; проверять через errors.Is. Принимает ключ числом, строкой или условием.
- **db.Take(&t)** — Одна запись без сортировки: только LIMIT 1. Ошибка not found та же — ErrRecordNotFound. Свой порядок задавайте Order(...) перед Take.
- **db.Last(&t)** — Последняя запись: ORDER BY id DESC LIMIT 1. Зеркало First по убыванию ключа.
- **db.Find(&todos)** — Срез записей без LIMIT и ORDER BY. ErrRecordNotFound не возвращает: пустая выборка — nil-ошибка и RowsAffected 0. Приёмник — срез или структура; срез ключей в условии разворачивается в id IN.
- **Pluck / Count** — Pluck вынимает одну колонку в срез (Distinct работает), Count считает строки в *int64 через count(*). Несколько колонок — уже Select + Scan.
- **Session(&gorm.Session{})** — Фикс chaining-ловушки: сессия возвращает переиспользуемый инстанс — условия следующих цепочек не липнут к Statement. Без неё хранить db.Where(...) в переменной нельзя.

### Что дальше по курсу

Чтение закрыло вторую букву CRUD. Дальше — условия: седьмой урок посвящён Where и операторам (IN, LIKE, диапазоны, структурные условия и их подводные камни); сегодня вы видели их в минимальной дозе, там будет полный арсенал. Затем обновления, удаление и мягкое удаление с gorm.DeletedAt — которое, кстати, уже сейчас молча добавляет к каждому чтению фильтр по deleted_at, если поле есть в модели. Если хочется освежить, откуда взялись таблица, индексы и уникальный Slug, вернитесь к [четвёртому уроку про миграции](/courses/gorm/lessons/04-migracii/); вставки и их SQL-следы разобраны в [пятом уроке](/courses/gorm/lessons/05-create/); полный план из 25 уроков — на [странице курса](/courses/gorm/). Домашка ниже закрепляет чтение целиком — и одну осознанную поломку инстанса.

## Коротко: что запомнить

- First, Take и Last достают одну запись: у всех троих LIMIT 1 и RaiseErrorOnNotFound; First сортирует по первичному ключу ASC, Last — DESC, Take не сортирует вовсе.
- db.First(&task, id) читает по ключу: inline-условия принимают число, строку и срез ключей (id IN); заполненный ключ в приёмнике сам становится условием.
- Пустой результат одиночного чтения — gorm.ErrRecordNotFound; каноническая проверка — errors.Is; обход без ошибки — db.Limit(1).Find, а не Find без Limit.
- db.Find(&todos) возвращает срез: пустая выборка — не ошибка, RowsAffected равен len; условия передаются inline или цепочкой Where.
- Структурные Where игнорируют нулевые поля: Where(&TodoItem{Priority: 0}) даст пустое условие и всю таблицу — нули включают картой или перечислением полей.
- Сканирование: колонка и поле связаны конвенцией; проекции — Select + Scan в лёгкую структуру; динамика — map[string]interface{}; Pluck — одна колонка в срез, Count — *int64.
- Ловушка chaining: инстанс после цепочки хранит условия, повторное использование склеивает их в запрос; лечится Session(&gorm.Session{}).

## Домашнее задание

Соберите локальный пример поверх таблицы из пятого урока: прочитайте первую и последнюю задачу через First и Last, распечатайте SQL-следы и сравните сортировки; достаньте запись по существующему и по несуществующему ключу — второй случай проверьте errors.Is(err, gorm.ErrRecordNotFound). Соберите срез открытых задач через Find с условием и убедитесь, что пустой фильтр не возвращает ошибку. Сделайте проекцию id + title в лёгкую структуру и первую строку в map[string]interface{}. Посчитайте открытые задачи через Where и Count. Финал: воспроизведите chaining-ловушку — сохраните db.Where в переменную, сделайте два запроса с разными условиями и распечатайте result.Statement.SQL второго, затем почините через Session(&gorm.Session{}).

## Вопросы и ответы по уроку

### Чем Take отличается от First, если ошибки у них одинаковые?

Только сортировкой: First добавляет ORDER BY по первичному ключу, Take не сортирует вообще. Когда порядок не важен или вы задаёте его собственным Order, Take честнее — не добавляет лишнего клоза. Флаг RaiseErrorOnNotFound и ErrRecordNotFound у обоих, как и у Last.

### Почему db.Find не возвращает ErrRecordNotFound, когда записей нет?

Find — финишер для наборов: пустой результат — штатное состояние, а не ошибка. Флаг RaiseErrorOnNotFound у него не включён, поэтому Error остаётся nil, RowsAffected — 0. Ошибка not found зарезервирована за одиночными финишерами First, Take и Last.

### Как безопасно переиспользовать запрос с условиями в разных хендлерах?

Сохраняйте сессию, а не цепочку: q := db.Where("done = ?", false).Session(&gorm.Session{}). Сессия возвращает инстанс, при использовании которого каждая новая цепочка получает копию Statement, а не дописывает условия в общий. Повторное использование инстанса без Session склеивает условия всех запросов — типовой источник «лишних» строк в выборках.
