Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 25 из 25 ~30 мин +50 XP

Итоговый проект: микроблог

Финал: всё, что вы собрали за двадцать четыре урока, уезжает в один проект — блог на ent целиком. Схема графа: пользователи-авторы, посты, комментарии, лайки и подписки; сервисный слой поверх клиента с транзакциями из шестнадцатого урока; миграции, которые поднимают базу; тесты на настоящей SQLite из двадцать третьего. По канону курса: анна, борис, вера, счётчик posts_count, мягкое удаление и группа go-ninja. Соберём композиционный корень main, посчитаем в таблице, из какого урока что пришло, и закроем курс выпускным экзаменом в домашке. Если вы искали «ent проект» или «микроблог go» — это тот урок.

Содержание урока · 13 разделов

Маршрут урока — от ТЗ до запуска. Сначала — карта графа: пять сущностей и их рёбра, требования списком. Затем — схемы User, Post, Comment и Like с индексами из двадцатого урока. Третья часть — сервисный слой: BlogService над *ent.Client, транзакции WithTx из шестнадцатого, лайки без гонок через ConstraintError из тринадцатого. Четвёртая — лента подписок: With-цепочки из одиннадцатого, ключсет из двенадцатого, интерсептор мягкого удаления из восемнадцатого. Пятая — композиционный корень main с миграцией Schema.Create из пятнадцатого и пулом из двадцать второго. Финал — тесты на enttest из двадцать третьего урока, таблица «из какого урока что пришло» и выпускной экзамен. Если вы искали «ent проект» или «блог на ent» — это тот урок.

ТЗ блога#

  • Пользователи: имя, уникальный email, возраст; авторы пишут посты и комментарии.
  • Посты: заголовок, тело, время создания, автор; мягкое удаление вместо жёсткого.
  • Комментарии: тело, автор, пост; умирают вместе с постом каскадом базы.
  • Лайки: пользователь лайкает пост не более одного раза — уникальная пара на уровне базы.
  • Подписки: направленные following/followers из шестого урока, дружба — симметричная.
  • Лента: посты тех, на кого подписан, новые сверху, по двадцать на страницу с курсором.
  • Счётчик posts_count у автора всегда согласован с числом его постов — без Cron и гонок.
  • Удалённые посты исчезают из всех выборок, но остаются в базе: интерсептор и частичный индекс.

Карта графа проекта#

СущностьПоляРёбраТаблица
Username, age, email*, posts_count, deleted_atposts (O2M), comments (O2M), friends (M2M), following/followers, likes (O2M)users
Posttitle, body, created_at, deleted_atauthor (M2O), comments (O2M), likes (O2M)posts
Commentbody, created_atpost (M2O), author (M2O)comments
Like—post (M2O), user (M2O); уникальная пара (post_id, user_id)likes
Рёбра-таблицыuser_friends: user_id, friend_idдрузья — симметричные строки, подписки — направленныеuser_friends

Схема User: канон курса

GO
type User struct{ ent.Schema }

func (User) Fields() []ent.Field {
        return []ent.Field{
                field.String("name").NotEmpty(),
                field.Int("age").Positive(),
                field.String("email").Unique(),
                field.Int("posts_count").Default(0),
                field.Time("deleted_at").Optional().Nillable(),
        }
}

func (User) Edges() []ent.Edge {
        return []ent.Edge{
                edge.To("posts", Post.Type),
                edge.To("comments", Comment.Type),
                edge.To("likes", Like.Type),
                edge.To("friends", User.Type),
                edge.To("following", User.Type).From("followers"),
        }
}

Канон пятого и шестого уроков: O2M на посты, комментарии и лайки, симметричная дружба одним ребром, направленные подписки одним билдером To и From. posts_count — денормализация из двенадцатого, её двигают хуки из семнадцатого; deleted_at — мягкое удаление из четырнадцатого, фильтр на чтение ставит интерсептор из восемнадцатого. Unique на email — уникальный индекс из двадцатого.

Схема Post: индекс ленты из двадцатого урока

GO
type Post struct{ ent.Schema }

func (Post) Fields() []ent.Field {
        return []ent.Field{
                field.String("title").NotEmpty(),
                field.Text("body"),
                field.Time("created_at").Default(time.Now),
                field.Time("deleted_at").Optional().Nillable(),
        }
}

func (Post) Edges() []ent.Edge {
        return []ent.Edge{
                edge.From("author", User.Type).Ref("posts").
                        Field("author_id").Required().Unique(),
                edge.To("comments", Comment.Type),
                edge.To("likes", Like.Type),
        }
}

func (Post) Indexes() []ent.Index {
        return []ent.Index{
                index.Edges("author").Fields("created_at").
                        Annotations(entsql.Desc(), entsql.IndexWhere("deleted_at IS NULL")).
                        StorageKey("posts_author_created_idx"),
        }
}

M2O-ребро с Field и Required из пятого урока: колонка author_id — NOT NULL, лента автора ищет именно её. Индекс — канон двадцатого урока: составной по author_id и created_at, убывающий — порядок дерева совпадает с «новые сверху», частичный — удалённые посты в дерево не попадают. StorageKey фиксирует имя: ревью миграций из пятнадцатого урока скажет спасибо.

Схемы Comment и Like: каскад и уникальная пара

GO
type Comment struct{ ent.Schema }

func (Comment) Fields() []ent.Field {
        return []ent.Field{
                field.Text("body").NotEmpty(),
                field.Time("created_at").Default(time.Now),
        }
}

func (Comment) Edges() []ent.Edge {
        return []ent.Edge{
                edge.From("post", Post.Type).Ref("comments").
                        Field("post_id").Required().Unique(),
                edge.From("author", User.Type).Ref("comments").
                        Field("author_id").Required().Unique(),
        }
}

type Like struct{ ent.Schema }

func (Like) Edges() []ent.Edge {
        return []ent.Edge{
                edge.From("post", Post.Type).Ref("likes").
                        Field("post_id").Required().Unique(),
                edge.From("user", User.Type).Ref("likes").
                        Field("user_id").Required().Unique(),
        }
}

func (Like) Indexes() []ent.Index {
        return []ent.Index{
                index.Edges("post", "user").Unique().
                        StorageKey("likes_post_user_uq"),
        }
}

Like — не join-таблица через Through, а полноценная сущность: уникальный индекс пары (post_id, user_id) — паттерн «одно на владельца» из двадцатого урока. Обе колонки NOT NULL благодаря Required — NULL-дыры уникальности нет. Каскадное поведение удалений настраивается на рёбрах из пятого урока: комментарии и лайки умирают вместе с постом силами базы.

Сервисный слой: контракты поверх клиента#

Граф собран — время сервиса. Границу из двадцать второго урока держим и здесь: BlogService получает *ent.Client в конструктор и наружу отдаёт методы домена, а не билдеры: CreateAuthor, Publish, Comment, ToggleLike, Follow, Feed, DeletePost. Ошибки ent переводятся в доменные в одном месте: ent.IsNotFound — к 404, ent.IsConstraintError — к конфликту «email занят» или «уже лайкнул»; хелперы сравнения ошибок из девятого и тринадцатого уроков не расползаются по хендлерам. Хуки из семнадцатого урока поддерживают posts_count автоматически, интерсептор из восемнадцатого прячет удалённые, privacy из девятнадцатого решает, кого пускать; сервис их не дублирует — они уже подключены на клиенте из схемы. Сервис тестопригоден без моков: под ним настоящий клиент из enttest, и тесты прогоняют настоящий SQL.

BlogService: создание автора и публикация

GO
type BlogService struct{ client *ent.Client }

func NewBlogService(c *ent.Client) *BlogService {
        return &BlogService{client: c}
}

func (s *BlogService) CreateAuthor(ctx context.Context, name, email string, age int) (*ent.User, error) {
        u, err := s.client.User.Create().
                SetName(name).SetEmail(email).SetAge(age).
                Save(ctx)
        if ent.IsConstraintError(err) {
                return nil, ErrEmailTaken // доменная ошибка: наверху -> 409
        }
        return u, err
}

func (s *BlogService) Publish(ctx context.Context, authorID int, title, body string) (*ent.Post, error) {
        var post *ent.Post
        err := WithTx(ctx, s.client, func(tx *ent.Tx) error {
                var err error
                post, err = tx.Post.Create().
                        SetTitle(title).SetBody(body).
                        SetAuthorID(authorID).
                        Save(ctx)
                return err // хук 17-го сдвинет posts_count в этой же транзакции
        })
        return post, err
}

WithTx из шестнадцатого урока делит код на «внутри транзакции» и «снаружи»: пост и инкремент счётчика коммитятся одним COMMIT — согласованность posts_count бесплатна, потому что её двигает хук из семнадцатого урока внутри той же мутации. ConstraintError переводится в доменную ошибку наверху: слой HTTP не знает про SQL. SetAuthorID возможен благодаря Field на ребре из пятого урока.

Проверка: posts_count без гонок

Два автора публикуют посты одновременно, у анны — сотый пост. Почему posts_count не врёт?

Комментарии и лайки: транзакции и ConstraintError#

Комментарий — формально одна мутация: Comment.Create().SetPostID(...).SetAuthorID(...) создаёт обе связи одним INSERT. Транзакция нужна, когда рядом с комментарием двигается счётчик или аудит-хук из семнадцатого урока пишет строку — тогда всё внутри WithTx, как учил шестнадцатый урок. Лайк сложнее: пара (post, user) уникальна, и «сначала прочитай, потом создай» — гонка: два быстрых клика создадут два лайка. Правильная схема — ограничения базы из двадцатого урока плюс ветвление по ошибке из тринадцатого: пробуем INSERT, на ent.IsConstraintError считаем, что лайк уже стоял, и снимаем его тем же запросом Delete().Where(). Гонки исчезают, потому что арбитр — не ваш код между чтением и записью, а UNIQUE-индекс, который проверяет база атомарно.

ToggleLike: лайк без гонок

GO
func (s *BlogService) ToggleLike(ctx context.Context, userID, postID int) (bool, error) {
        err := s.client.Like.Create().
                SetUserID(userID).SetPostID(postID).
                Save(ctx)
        switch {
        case err == nil:
                return true, nil // поставили лайк
        case ent.IsConstraintError(err):
                _, err := s.client.Like.Delete().
                        Where(like.UserIDEQ(userID), like.PostIDEQ(postID)).
                        Exec(ctx)
                return false, err // сняли лайк
        default:
                return false, err // настоящая ошибка базы — наверх
        }
}

Первый Create вставляет строку — лайк стоит. Второй упирается в уникальную пару likes_post_user_uq, IsConstraintError срабатывает, ветка Delete удаляет строку — Toggle вернул состояние обратно. Заметьте: OnConflictColumns из тринадцатого урока тоже подошёл бы, но для переключателя «поставил-снял» честнее явная ветка удаления, чем UpdateNewValues.

Второй лайк

Прочитайте код и выберите, что он выведет:

st1, err1 := svc.ToggleLike(ctx, anna.ID, post.ID)
st2, err2 := svc.ToggleLike(ctx, anna.ID, post.ID)
n, _ := svc.LikeCount(ctx, post.ID)

fmt.Println(st1, err1 == nil, st2, err2 == nil, n)

Лента подписок: граф целиком#

Лента — витрина всего курса: посты тех, на кого подписан пользователь, свежие сверху, живые, с авторами и комментариями. Запрос начинается от Post: Where(post.HasAuthorWith(user.HasFollowersWith(user.IDEQ(me)))) — предикаты по связям из десятого урока читаются снаружи внутрь. Загрузка — With-цепочка из одиннадцатого: WithAuthor — имя и счётчик автора, WithComments с Limit(3) и вложенным WithAuthor — последние комментарии с их авторами. Пагинация — ключсет из двенадцатого урока: CreatedAtLT плюс IDGT, чтобы равные времена не дублировали страницы. Мягкое удаление в запросе не упоминается ни разу: интерсептор из восемнадцатого вшивает DeletedAtIsNil во всякий обход графа — и в корень, и в With-загрузки. Цена известна заранее: один корневой SELECT плюс по одному на уровень With — счётчик запросов из двадцать четвёртого это докажет.

Feed: несколько запросов на страницу

GO
// type Cursor struct { Time time.Time; ID int }

func (s *BlogService) Feed(ctx context.Context, me int, cur *Cursor) ([]*ent.Post, error) {
        q := s.client.Post.Query().
                Where(post.HasAuthorWith(user.HasFollowersWith(user.IDEQ(me)))).
                WithAuthor(func(aq *ent.UserQuery) {
                        aq.Select(user.FieldName, user.FieldPostsCount)
                }).
                WithComments(func(cq *ent.CommentQuery) {
                        cq.Order(ent.Desc(comment.FieldCreatedAt)).Limit(3).
                                WithAuthor(func(aq *ent.UserQuery) {
                                        aq.Select(user.FieldName)
                                })
                }).
                Order(ent.Desc(post.FieldCreatedAt), ent.Asc(post.FieldID)).
                Limit(20)
        if cur != nil {
                q = q.Where(post.CreatedAtLT(cur.Time), post.IDGT(cur.ID))
        }
        return q.All(ctx)
}

Один корневой SELECT плюс по одному на уровень With: авторы, комментарии, их авторы — четыре запроса на страницу, сколько бы подписок ни было. Select на соседях безопасен: ent сам допишет ключи, без которых рёбра не соберутся. Интерсептор мягкого удаления уже внутри: DeletedAtIsNil вшит и в корень, и в WithAuthor, и в WithComments.

Путь запроса GET /feed

Соберите путь запроса ленты от хендлера до курсора.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Миграции и композиционный корень#

Проект начинается не с кода, а с базы, которую поднимет Schema.Create из пятнадцатого урока: automigrate сверяет схемы с таблицами и дописывает недостающее — первый прогон на чистой базе создаёт всё, второй подряд ничего не меняет; идемпотентность эту проверяли тесты двадцать третьего урока, и здесь она станет частью приёмки. Versioned-миграции Atlas — следующий шаг, когда изменения схем нужно ревьюить файлами. Композиционный корень собирает всё ровно один раз: пул соединений из двадцать второго урока, клиент, миграция, сервис, хендлеры. Хуки и интерсепторы объявлены в схеме — в main их не видно, и это хорошо: корень не обязан знать про posts_count и DeletedAtIsNil. Конфигурация с честным падением: пустой DSN — это fatal при старте, а не тайна на третьем экране продакшена.

main.go: весь проект в двадцати строках

GO
func main() {
        ctx := context.Background()
        dsn := os.Getenv("BLOG_DSN")
        if dsn == "" {
                log.Fatal("BLOG_DSN пуст: старт невозможен")
        }

        db, err := sql.Open(dialect.Postgres, dsn)
        if err != nil {
                log.Fatal(err)
        }
        db.SetMaxOpenConns(25)
        db.SetConnMaxLifetime(30 * time.Minute)

        client := ent.NewClient(ent.Driver(entsql.OpenDB(dialect.Postgres, db)))
        if err := client.Schema.Create(ctx); err != nil {
                log.Fatal(err)
        }
        svc := NewBlogService(client)

        mux := http.NewServeMux()
        mux.HandleFunc("POST /authors", handleCreateAuthor(svc))
        mux.HandleFunc("POST /posts", handlePublish(svc))
        mux.HandleFunc("POST /posts/{id}/like", handleToggleLike(svc))
        mux.HandleFunc("GET /feed", handleFeed(svc))

        log.Println("блог на ent слушает :8080")
        log.Fatal(http.ListenAndServe(":8080", mux))
}

Композиционный корень: конфиг с честным падением, пул, клиент, миграция, сервис, роутер. Хендлеры — тонкие адаптеры: разобрать запрос, вызвать метод сервиса, перевести доменные ошибки в статусы — IsNotFound в 404, ErrEmailTaken в 409. Полные HTTP-слои с валидацией и JWT строятся в курсах Gin и Echo — слой данных уже готов.

Тесты проекта: контур из двадцать третьего урока#

Тесты не пишутся заново — они уже спроектированы. enttest.Open поднимает SQLite с automigrate и DSN с _fk=1, фикстура seedMicroblog создаёт канон — анну, бориса, веру и их связи, table-driven проверяет фильтры, ent.IsConstraintError — дубль email и второй лайк, атомарность хука posts_count проверяется сбоем посреди WithTx, невидимость удалённых — интерсептором. Новое только масштаб: тесты гоняют сервис, а не сырые билдеры — фикстура возвращает BlogService поверх клиента, и тест ленты доказывает на настоящем SQL, что анна видит посты бориса, на которого подписана, но не посты веры. Каскад удалений комментариев при этом исполняется базой — поведение ребра из пятого урока — а не хуком. go test ./... -race обязателен: ToggleLike из двух горутин на одном посте обязан оставить ровно один лайк или ни одного, но не два.

Тест ленты на сервисе

GO
func TestFeedFollowsOnly(t *testing.T) {
        client := enttest.Open(t, dialect.SQLite,
                fmt.Sprintf("file:test_%s?mode=memory&_fk=1", t.Name()))
        defer client.Close()

        svc := NewBlogService(client)
        users := seedMicroblog(t, client) // анна, борис, вера + подписки

        page, err := svc.Feed(ctx, users.anna.ID, nil)
        if err != nil {
                t.Fatal(err)
        }
        if len(page) == 0 {
                t.Fatal("лента анны пуста: подписка на бориса потерялась")
        }
        for _, p := range page {
                if p.Edges.Author.Name == "вера" {
                        t.Fatal("пост веры не должен попадать в ленту анны")
                }
        }
}

Фикстура из двадцать третьего урока экономит пол-теста: анна с постами, борис, вера, подписки анны на бориса. Ассерт на границе графа — самый ценный: он ловит и испорченный предикат HasFollowersWith, и снятый интерсептор одновременно. WithAuthor уже внутри Feed, поэтому Edges.Author заполнен без отдельного запроса.

Проверка: что докажет тест с _fk=1

Тест удаляет пост бориса и проверяет, что его комментарии исчезли. Что обеспечивает это поведение?

Соберите композиционный корень

Впишите открытие клиента, миграцию и диагностику.

client, err := .Open(dialect.Postgres, dsn) if err != nil { log.Fatal(err) } if err := client..Create(ctx); err != nil { log.Fatal(err) } client = client.() // считать запросы глазами на старте проекта svc := NewBlogService(client)

Из какого урока что пришло#

Что в проектеУроки курса
Поля, валидаторы, уникальный email, Optional-NULL03-поля, 04-optional
Рёбра O2M и M2O, Required, Field, каскады удалений05-edges-o2m
Друзья и подписки following/followers06-edges-m2m
Билдеры Create, OnConflict, SetAuthorID08-создание
Предикаты по связям HasAuthorWith10-предикаты
With-цепочки ленты, проекции соседей11-with, 24-производительность
Ключсет-пагинация, курсор (created_at, id)12-пагинация
posts_count: Add-мутаторы и хуки13-update, 17-хуки
Мягкое удаление и DeletedAtIsNil14-delete, 18-interceptors
Schema.Create, идемпотентность, Atlas15-миграции
WithTx и атомарность счётчика16-транзакции
Индексы ленты, уникальная пара лайка, StorageKey20-indeksy
enttest, фикстуры seedMicroblog, -race23-тестирование

Грабли выпускника#

  • Schema.Create в хендлере: миграция — старт приложения, не горячий путь.
  • Счётчик через чтение-запись: только Add-мутаторы и хуки, иначе гонки вернутся.
  • Лайк без уникальной пары: два клика — два лайка; «прочитал-создал» — гонка.
  • With-цепочка на всё сразу: грузите то, что рисуется; проекции соседей дешевле.
  • OFFSET вместо ключсета: страницы замедляются с глубиной.
  • Хук без ветки ent.BatchMutation: переход на CreateBulk молча ломает posts_count.
  • Тесты на моках: SQL не мокается — enttest и настоящая база из двадцать третьего урока.
  • Индексы на всякий случай: каждая штука — цена на записи; индекс появляется от запроса.

Проверка: ent или не ent

Ваш следующий проект — каталог с десятью связанными сущностями и еженедельными правками схемы. Что советует курс?

Что дальше#

Курс закрыт: вы прошли путь от «что такое ent» до полного блога — граф из пяти сущностей, сервис с транзакциями, тесты на настоящей базе, миграции и производительность. Дальше три дороги. GraphQL-слой поверх этого же графа через entgql из двадцать первого урока — схемы и резолверы сгенерируются из тех же Schema-файлов. Мульти-тенантность из двадцать второго, если блог станет платформой: клиенты и интерсепторы уже к ней готовы. И соседние курсы: GORM и sqlc решают те же задачи слоя данных другими инструментами, HTTP-слой собирают Gin, Echo и Fiber. Полный план из 25 уроков — на странице курса, микроблог поднимался с первого урока. Домашка ниже — выпускной экзамен: семь заданий, которые соберут курс в одну приёмку.

Карточки: проект «микроблог»

Термин

Карта графа блога

нажмите, чтобы перевернуть
Определение

User (name, age, email*, posts_count, deleted_at) — posts, comments, likes, friends, following/followers; Post (title, body, created_at, deleted_at) — author, comments, likes; Comment (body, created_at) — post, author; Like — пара (post, user) с уникальным индексом. Таблицы: users, posts, comments, likes, user_friends.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Проект собрал курс воедино: граф User/Post/Comment/Like с индексами ленты, сервисный слой, транзакции, тесты и миграции.
  • Схемы из третьего-шестого уроков: поля с валидаторами, O2M и M2O-рёбра с Required и каскадами, друзья и following/followers, денормализованный posts_count.
  • Индексы из двадцатого урока: составной (author_id, created_at) с Desc и IndexWhere для ленты, уникальная пара лайка — «одно на владельца».
  • Сервис прячет ent за доменными методами и ошибками: IsNotFound — 404, IsConstraintError — 409; хуки и интерсепторы живут на клиенте из схемы.
  • Транзакции шестнадцатого урока держат атомарность: пост и счётчик одним COMMIT; ToggleLike без гонок на ConstraintError.
  • Лента — With-цепочки, предикаты по связям и ключсет: четыре запроса на страницу, удалённое невидимо благодаря интерсептору.
  • Композиционный корень main: пул, клиент, Schema.Create при старте, тонкие хендлеры; GraphQL из двадцать первого — следующий слой над тем же графом.
  • Тесты из двадцать третьего урока: enttest, фикстуры, -race; выпускной экзамен в домашке собирает приёмку проекта.

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

Выпускной экзамен: соберите блог на ent целиком и пройдите приёмку. Первое: схемы User, Post, Comment и Like из урока — с индексом ленты и уникальной парой лайка; прогоните go generate и Schema.Create дважды: второй прогон обязан ничего не менять (идемпотентность миграции). Второе: BlogService с методами CreateAuthor, Publish, Comment, ToggleLike, Follow и Feed; дубль email обязан вернуть доменную ошибку из ent.IsConstraintError, а не сырую ConstraintError. Третье: подключите хуки posts_count из семнадцатого урока и докажите тестом атомарность: ошибка посреди WithTx откатывает и пост, и счётчик; затем проверьте, что хук переживёт CreateBulk — ветка ent.BatchMutation и Mutations() обязательна. Четвёртое: лента — предикаты по связям, With-цепочки, ключсет; тестом докажите, что анна видит посты бориса, но не веры, и что мягко удалённый пост не виден ни в ленте, ни в загрузке через ребро. Пятое: ToggleLike дважды — счётчик лайков обязан вернуться в ноль; затем тот же ToggleLike из двух горутин под go test -race на одном посте: итог один лайк или ноль, но не два. Шестое: замерьте страницу ленты счётчиком запросов из двадцать четвёртого урока — на ней не должно быть N+1; замените OFFSET на ключсет, если ещё не сделали этого. Седьмое: сравните подход с соседями — [GORM](/courses/gorm/) и [sqlc](/courses/sqlc/) решают те же задачи иначе; напишите абзац, что выбрали бы для следующего проекта и почему. Приёмка-контроль: go vet ./..., go test ./... -race, второй Schema.Create без изменений, счётчик запросов ленты не больше четырёх — вся приёмка зелёная, значит курс сдан. Сдал — вы знаете ent.

Личные заметкиПусто
Хранятся только в вашем браузере (ent · Итоговый проект: микроблог)

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

Достаточно ли этого проекта для портфолио?

Как слой данных — да: граф с пятью сущностями, транзакции, тесты на настоящей базе и миграции показывают владение ent на уровне, который встречается нечасто. Для полного приложения добавьте HTTP-слой из курсов Gin или Echo, Docker из их уроков деплоя — и проект станет резюме-кейсом «от схемы до продакшена».

SQLite или Postgres для этого проекта?

Разрабатывать можно на SQLite из двадцать третьего урока — скорость контура тестов важнее. Деплоить — на Postgres: диалектные вещи вроде частичных индексов из двадцатого урока гоняйте на настоящем Postgres в CI. ent мультибазовый с седьмого урока: меняется DSN и драйвер, схема не меняется.

Как добавить роли и права к блогу?

Privacy-политики из девятнадцатого урока поверх клиента: viewer-контекст, DenyIfNoViewer, AllowIfAdmin — сервис и хендлеры менять не придётся, правила уже исполняются на каждом запросе и мутации. Транспортный слой (JWT, middleware) подставляет viewer в контекст — это материал курсов Gin и Echo.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

go-kit25 уроков~14 чпродвинутый25 уроков уже доступны

go-kit: микросервисный тулкит

Курс go-kit: endpoint, транспорт, логирование, метрики, трейсинг, circuit breaker, service discovery. 25 уроков с итоговым микросервисом.

  • Понимать архитектуру endpoint → transport → service
  • Оборачивать сервисы middleware: логи, метрики, трейсинг
  • Подключать HTTP и gRPC транспорты
  • Ставить rate limiting и circuit breaker