---
title: "Модели и соглашения: gorm.Model, теги, именование"
description: "Модели GORM: соглашения об именовании таблиц и колонок, встроенный gorm.Model, теги column, size, uniqueIndex, метод TableName и NamingStrategy, типы полей."
course: "GORM: ORM-библиотека для Go"
url: "https://golangmaster.ru/courses/gorm/lessons/03-modeli/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Модели и соглашения: gorm.Model, теги, именование

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

GORM ничего не знает о ваших данных, пока вы не объявили структуру — модель это и есть весь контракт между Go и таблицей. Разберём соглашения, из которых растут users вместо User и full_name вместо FullName, встроенный gorm.Model с четырьмя служебными полями, теги колонок и индексов, переопределение имён через TableName и NamingStrategy — и ловушку, из-за которой опечатка в теге молча не срабатывает.

Открываете базу после первых экспериментов, а там таблица todo_items и колонка full_name — откуда? Это не GORM капризничает, это соглашения сработали. Модель в GORM — самая обычная структура Go: никакого базового класса, никакой кодогенерации, никаких DSL. GORM один раз читает поля через рефлексию при первом обращении к модели, кэширует распарсенную схему и дальше работает с ней без затрат. Ключевая идея библиотеки — соглашения вместо конфигурации: если структура называется User, таблица будет users; если поле FullName, колонка будет full_name. Правила простые, но их надо знать наизусть, потому что по ним живёт каждый запрос. А когда соглашение мешает — есть два уровня ручной настройки: теги на полях и стратегия именования.

### Соглашения об именовании: users, full_name, todo_items

Три правила. Первое: имя таблицы — snake_case во множественном числе от имени структуры. User превращается в users, TodoItem — в todo_items, Address — в addresses (склонение делает библиотека inflection, и она знает про нерегулярные формы вроде person → people). Второе: имя колонки — snake_case от имени поля. FullName становится full_name, CreatedAt — created_at. Третье: Go-аббревиатуры GORM распознаёт и не разрывает — UserID превратится в user_id, а не в u_s_e_r_i_d, APIKey — в api_key, HTTPStatus — в http_status. Список «понятных» аббревиатур зашит в код: ID, API, URL, HTTP, JSON, SQL, UUID и другие.

Эти же правила работают на автоматические таблицы связей many-to-many — их мы будем разбирать в уроке 13, и там знание конвенций прямо спасает от сюрпризов. Пока достаточно запомнить: всё, что Go пишет CamelCase, база видит в snake_case, а единственное число становится множественным на уровне таблицы.

### Структура и то, что получит база

```go
type TodoItem struct {
        ID      uint
        Title   string
        Done    bool
        DueDate time.Time
}

// Таблица: "todo_items"
// Колонки: id, title, done, due_date
```

Ни один тег не написан — GORM выведет всё из имён. ID получил роль первичного ключа автоматически: поле с именем ID и типом uint распознаётся как автоинкрементный ключ без явного primaryKey. Комментарий внизу — то, что создаст AutoMigrate, о котором подробнее в уроке 04.

### gorm.Model: что встраивает по умолчанию

Почти каждая модель в реальных проектах начинается со встраивания gorm.Model — стандартного набора служебных полей. Вот его настоящее определение из исходников:

```go
type Model struct {
        ID        uint           `gorm:"primaryKey"`
        CreatedAt time.Time
        UpdatedAt time.Time
        DeletedAt gorm.DeletedAt `gorm:"index"`
}

type Post struct {
        gorm.Model // встраивание — поля «врастают» в Post
        Title string
        Body  string
}

// Таблица "posts" получит колонки:
// id, created_at, updated_at, deleted_at, title, body
```

Четыре поля, четыре обязанности. ID — автоинкрементный первичный ключ. CreatedAt GORM заполняет текущим временем при вставке, UpdatedAt — при каждом обновлении: руками трогать их не нужно. DeletedAt — самый хитрый: тип gorm.DeletedAt включает мягкое удаление — после Delete строка не удаляется, а лишь помечается временем, и обычные SELECT-запросы перестают её видеть. Подробно разберём в уроке 09; здесь важно, что колонка снабжена индексом, потому что deleted_at попадает почти в каждый запрос.

### Теги полей: колонки, индексы, ограничения

Когда имя поля недостаточно или нужна особая логика, в игру входят структурные теги. Тег gorm — строка с настройками через точку с запятой, каждая вида ключ или ключ:значение. Теги читаются при разборе схемы — то есть один раз, при первом использовании модели. Самые ходовые: column переименовать колонку, size задать размер текстового типа, uniqueIndex повесить уникальный индекс, index — обычный, type вписать SQL-тип вручную, default указать значение по умолчанию, not null запретить пустоты, а дефис — полностью выкинуть поле из таблицы (удобно для кэша и вычисляемых вещей).

### Частые теги GORM

| Тег | Действие | Пример |
| --- | --- | --- |
| primaryKey | поле становится первичным ключом | gorm:"primaryKey" |
| column | переименовать колонку | gorm:"column:description" |
| size | размер строкового типа | gorm:"size:64" |
| type | явный SQL-тип | gorm:"type:decimal(10,2)" |
| uniqueIndex | уникальный индекс | gorm:"uniqueIndex" |
| index | обычный индекс | gorm:"index" |
| default | значение по умолчанию | gorm:"default:0" |
| not null | запрет NULL | gorm:"not null" |
| - | исключить поле из таблицы | gorm:"-" |

### Модель каталога с тегами

```go
type Product struct {
        gorm.Model
        SKU   string  `gorm:"size:64;uniqueIndex"`
        Title string  `gorm:"not null"`
        Price float64 `gorm:"type:decimal(10,2)"`
        Note  string  `gorm:"column:description;default:''"`
        Cache string  `gorm:"-"`
}

// В таблице "products" появятся:
// id, created_at, updated_at, deleted_at,
// sku (varchar(64), unique), title (not null),
// price (decimal(10,2)), description (default '')
```

SKU короткий и обязанный быть уникальным — size:64 плюс uniqueIndex. Price — деньги, значит точный тип decimal, а не double, который накапливает ошибку округления. Note переименована в description тегом column и получает дефолт. Cache вообще не попадёт в базу — это поле времени исполнения.

### TableName и NamingStrategy: своя логика именования

Соглашения — это дефолт, а не закон. Метод TableName возвращает имя таблицы напрямую и имеет высший приоритет: хватит, когда работаете с legacy-схемой, где таблицы называются по-другому, или когда компания держит префиксы. Если правило глобальное (например, все таблицы с префиксом shop_), вместо десятка методов TableName настраивается NamingStrategy в конфиге подключения: TablePrefix добавляет префикс ко всем таблицам, SingularTable запрещает множественное число — структура User будет мапиться на user, а не users. Есть и NameReplacer для точечных замен имён. Стратегию передают в gorm.Open через поле NamingStrategy конфига, тип берётся из пакета gorm.io/gorm/schema.

### Переопределяем имена

```go
// Точечно: для одной модели
func (Product) TableName() string {
        return "catalog_products"
}

// Глобально: для всех моделей
import "gorm.io/gorm/schema"

db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
        NamingStrategy: schema.NamingStrategy{
                TablePrefix:   "shop_",
                SingularTable: true,
        },
})
// Product теперь живёт в "shop_product"
```

Метод TableName объявляют на значении-приёмнике, не на указателе — GORM вызывает его и на значениях. После включения TablePrefix все таблицы получат префикс shop_, а SingularTable оставит имена в единственном числе. Миграции и запросы подхватят новые имена автоматически.

> **Опечатка в теге не вызывает ошибку.** Написали uniquIndex вместо uniqueIndex? Никто не скажет ни слова: незнакомые ключи GORM молча пропускает, схема соберётся без уникального индекса, а первый дубликат потом уйдёт в базу. Я не доверяю тегам на глаз: после первой миграции смотрю фактическую схему или проверяю в тесте через db.Migrator().HasIndex — об этом в уроке 04.

### Типы полей: от string до time.Time

Тип колонки GORM выводит из типа поля, а точный SQL-тип зависит от диалекта. Строка без size превращается в text (Postgres) или varchar нужного размера; int — в bigint, bool — в boolean, float64 — в double precision, time.Time — в timestamptz, []byte — в binary/blob. Правило денег повторю отдельно: для сумм используйте явный тип:decimal или целые копейки в int — float64 в базе — источник «копеечных» расхождений в бухгалтерии.

Отдельная история — отсутствующие значения. Обычный string не различает пустую строку и NULL: оба превратятся в "". Если колонка допускает NULL, поле делают указателем (*string, *time.Time) — GORM запишет NULL, когда указатель nil. Альтернатива — типы sql.NullString и compañía из стандартной библиотеки; сравнение обоих подходов и третья опция, sql.Null[T] из Go 1.22, ждут вас в уроке 21. А если хочется хранить в колонке структуру или карту — это тег serializer:json и пользовательские типы, урок 22.

### Проверка: соглашения об именовании

**Вопрос:** Как GORM назовёт таблицу для структуры TodoItem?

- todo_item
- ✅ todo_items
- TodoItem
- todoitem

Имя таблицы — snake_case во множественном числе: TodoItem → todo_items. Склонение делает пакет inflection; для нерегулярных форм (person → people) он тоже знает ответ.

### Проверка: молчаливые теги

**Вопрос:** Поле помечено gorm:"uniquIndex" (опечатка). Что произойдёт при миграции?

- AutoMigrate вернёт ошибку о неизвестном теге
- Колонка станет NOT NULL
- ✅ Тег молча проигнорируется — уникального индекса не будет
- Поле не попадёт в таблицу

Незнакомые ключи тега GORM пропускает без предупреждений — это самая коварная категория багов в моделях. Поле попадёт в таблицу как обычно, просто без индекса. Проверяйте фактическую схему после миграции.

### Какими будут имя таблицы и колонка?

```go
type Order struct {
        gorm.Model
        OrderNo string `gorm:"size:32;uniqueIndex"`
        Total   float64
}

// Как назовутся таблица и колонка OrderNo?
```

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

- ✅ таблица orders, колонка order_no
- таблица order, колонка order_no
- таблица orders, колонка OrderNo
- таблица orders, колонка o_r_d_e_r_no

Таблица — множественное число от структуры: orders. Поле OrderNo разбивается на слова Order + No и превращается в order_no: разбиение идёт по границам CamelCase, аббревиатуры из списка GORM вроде ID не рвутся, а короткое No — не аббревиатура, а обычное слово.

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

Восстановите теги и метод переопределения имени таблицы: у Slug — размер 64 и уникальный индекс, у Views — дефолт 0, таблица — blog_articles.

```go
type Article struct {
        gorm.Model
        Slug  string `gorm:"___:64;unique___"`
        Title string
        Views int    `gorm:"___:0"`
}

func (Article) ___() string {
        return "blog_articles"
}
```

Подсказка: Размер строки задаёт size, уникальный индекс — uniqueIndex, значение по умолчанию — default. Имя таблицы возвращает метод TableName.

Правильные ответы в пропусках: `size`, `Index`, `default`, `TableName`

### Карточки: словарь моделей

- **Соглашение об именовании таблиц** — snake_case во множественном числе от имени структуры: User → users, TodoItem → todo_items. Глобально меняется NamingStrategy, точечно — метод TableName.
- **Соглашение об именовании колонок** — snake_case от имени поля: FullName → full_name, UserID → user_id (известные аббревиатуры вроде ID, API, HTTP не разрываются).
- **gorm.Model** — Встраиваемая структура: ID (primaryKey), CreatedAt, UpdatedAt (автозаполнение) и DeletedAt с индексом — мягкое удаление.
- **Тег column** — Переименовывает колонку: gorm:"column:description" — поле Note ляжет в колонку description.
- **Тег uniqueIndex** — Создаёт уникальный индекс на колонке. Опечатка в ключе тега молча игнорируется — схема остаётся без индекса.
- **gorm:"-"** — Поле исключается из таблицы: не мигрирует, не читается и не пишется. Для кэшей и вычисляемых значений.

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

Модель описана — пора её материализовать. В четвёртом уроке — [автомиграции](/courses/gorm/lessons/04-migracii/): что делает AutoMigrate, чего он не умеет и когда нужны версионированные миграции. В пятом и шестом — [создание записей](/courses/gorm/lessons/05-create/) и [чтение через First и Find](/courses/gorm/lessons/06-read/). Теги индексов мы использовали мельком — в уроке 18 разберём индексы всерьёз: составные, частичные и как проверить план запроса. А если хочется видеть, как модели живут в настоящем проекте, — финальный урок курса собирает слой данных блога целиком.

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

- Модель — обычная структура Go: GORM разбирает её рефлексией один раз и кэширует схему.
- Таблица — множественное число в snake_case, колонка — snake_case поля; UserID даёт user_id.
- gorm.Model добавляет ID, CreatedAt, UpdatedAt и DeletedAt с индексом (мягкое удаление).
- Теги уточняют схему: column, size, type, uniqueIndex, index, default, not null, дефис для исключения поля.
- TableName() переопределяет имя таблицы точечно, NamingStrategy — глобально (префиксы, единственное число).
- Опечатка в теге не вызывает ошибку — проверяйте фактическую схему после миграции.
- Для денег — decimal или целые копейки; NULL моделируется указателями или sql.Null* (урок 21).

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

Объявите модель Invoice: gorm.Model, поле Number с size:32 и уникальным индексом, Amount с типом decimal(12,2), поле internalMemo с тегом дефис. Локально подключите SQLite, выполните AutoMigrate и откройте файл базы (sqlite3 app.db, затем .schema invoices): убедитесь, что колонки называются так, как вы ожидали, уникальный индекс существует, а internalMemo в таблице нет.

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

### Обязательно ли встраивать gorm.Model?

Нет: модель может быть любой структурой без gorm.Model — тогда не будет автотаймстампов и мягкого удаления, а первичным ключом станет поле с именем ID или тег primaryKey. Для таблиц-логов и join-таблиц чистые структуры — норм.

### Как назвать колонку, если поле называется APIKey?

api_key: GORM знает список Go-аббревиатур (ID, API, URL, HTTP, JSON и другие) и не рвёт их на буквы. Полный список — в исходниках пакета gorm.io/gorm/schema.

### Что делать, если таблица уже существует со «странными» именами?

Переопределите имена точечно: метод TableName() для таблицы и тег column для колонок. Либо настройте NamingStrategy, если закономерность одна на весь проект — например, префикс legacy_.
