Содержание урока · 13 разделов
- ТЗ блога
- Карта графа проекта
- Сервисный слой: контракты поверх клиента
- Комментарии и лайки: транзакции и ConstraintError
- Лента подписок: граф целиком
- Миграции и композиционный корень
- Тесты проекта: контур из двадцать третьего урока
- Из какого урока что пришло
- Грабли выпускника
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Маршрут урока — от ТЗ до запуска. Сначала — карта графа: пять сущностей и их рёбра, требования списком. Затем — схемы User, Post, Comment и Like с индексами из двадцатого урока. Третья часть — сервисный слой: BlogService над *ent.Client, транзакции WithTx из шестнадцатого, лайки без гонок через ConstraintError из тринадцатого. Четвёртая — лента подписок: With-цепочки из одиннадцатого, ключсет из двенадцатого, интерсептор мягкого удаления из восемнадцатого. Пятая — композиционный корень main с миграцией Schema.Create из пятнадцатого и пулом из двадцать второго. Финал — тесты на enttest из двадцать третьего урока, таблица «из какого урока что пришло» и выпускной экзамен. Если вы искали «ent проект» или «блог на ent» — это тот урок.
ТЗ блога#
- Пользователи: имя, уникальный email, возраст; авторы пишут посты и комментарии.
- Посты: заголовок, тело, время создания, автор; мягкое удаление вместо жёсткого.
- Комментарии: тело, автор, пост; умирают вместе с постом каскадом базы.
- Лайки: пользователь лайкает пост не более одного раза — уникальная пара на уровне базы.
- Подписки: направленные following/followers из шестого урока, дружба — симметричная.
- Лента: посты тех, на кого подписан, новые сверху, по двадцать на страницу с курсором.
- Счётчик posts_count у автора всегда согласован с числом его постов — без Cron и гонок.
- Удалённые посты исчезают из всех выборок, но остаются в базе: интерсептор и частичный индекс.
Карта графа проекта#
| Сущность | Поля | Рёбра | Таблица |
|---|---|---|---|
| User | name, age, email*, posts_count, deleted_at | posts (O2M), comments (O2M), friends (M2M), following/followers, likes (O2M) | users |
| Post | title, body, created_at, deleted_at | author (M2O), comments (O2M), likes (O2M) | posts |
| Comment | body, created_at | post (M2O), author (M2O) | comments |
| Like | — | post (M2O), user (M2O); уникальная пара (post_id, user_id) | likes |
| Рёбра-таблицы | user_friends: user_id, friend_id | друзья — симметричные строки, подписки — направленные | user_friends |
Схема User: канон курса
GOtype 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: индекс ленты из двадцатого урока
GOtype 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: каскад и уникальная пара
GOtype 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: создание автора и публикация
GOtype 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: лайк без гонок
GOfunc (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: весь проект в двадцати строках
GOfunc 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 из двух горутин на одном посте обязан оставить ровно один лайк или ни одного, но не два.
Тест ленты на сервисе
GOfunc 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
Тест удаляет пост бориса и проверяет, что его комментарии исчезли. Что обеспечивает это поведение?
Соберите композиционный корень
Впишите открытие клиента, миграцию и диагностику.
Из какого урока что пришло#
| Что в проекте | Уроки курса |
|---|---|
| Поля, валидаторы, уникальный email, Optional-NULL | 03-поля, 04-optional |
| Рёбра O2M и M2O, Required, Field, каскады удалений | 05-edges-o2m |
| Друзья и подписки following/followers | 06-edges-m2m |
| Билдеры Create, OnConflict, SetAuthorID | 08-создание |
| Предикаты по связям HasAuthorWith | 10-предикаты |
| With-цепочки ленты, проекции соседей | 11-with, 24-производительность |
| Ключсет-пагинация, курсор (created_at, id) | 12-пагинация |
| posts_count: Add-мутаторы и хуки | 13-update, 17-хуки |
| Мягкое удаление и DeletedAtIsNil | 14-delete, 18-interceptors |
| Schema.Create, идемпотентность, Atlas | 15-миграции |
| WithTx и атомарность счётчика | 16-транзакции |
| Индексы ленты, уникальная пара лайка, StorageKey | 20-indeksy |
| enttest, фикстуры seedMicroblog, -race | 23-тестирование |
Грабли выпускника#
- Schema.Create в хендлере: миграция — старт приложения, не горячий путь.
- Счётчик через чтение-запись: только Add-мутаторы и хуки, иначе гонки вернутся.
- Лайк без уникальной пары: два клика — два лайка; «прочитал-создал» — гонка.
- With-цепочка на всё сразу: грузите то, что рисуется; проекции соседей дешевле.
- OFFSET вместо ключсета: страницы замедляются с глубиной.
- Хук без ветки ent.BatchMutation: переход на CreateBulk молча ломает posts_count.
- Тесты на моках: SQL не мокается — enttest и настоящая база из двадцать третьего урока.
- Индексы на всякий случай: каждая штука — цена на записи; индекс появляется от запроса.
Проверка: ent или не ent
Ваш следующий проект — каталог с десятью связанными сущностями и еженедельными правками схемы. Что советует курс?
Песочницы в курсе ent нет — и финальному проекту она не нужна: соберите блог в своём go-проекте. Порядок: четыре схемы из урока, go generate, Schema.Create на чистой базе, BlogService с методами из урока, enttest-тесты на фикстуре. Затем идите к выпускному экзамену из домашки ниже: он собирает проверки из ключевых уроков, и каждая из них либо зелёная, либо указывает, какой урок перечитать.
Что дальше#
Курс закрыт: вы прошли путь от «что такое ent» до полного блога — граф из пяти сущностей, сервис с транзакциями, тесты на настоящей базе, миграции и производительность. Дальше три дороги. GraphQL-слой поверх этого же графа через entgql из двадцать первого урока — схемы и резолверы сгенерируются из тех же Schema-файлов. Мульти-тенантность из двадцать второго, если блог станет платформой: клиенты и интерсепторы уже к ней готовы. И соседние курсы: GORM и sqlc решают те же задачи слоя данных другими инструментами, HTTP-слой собирают Gin, Echo и Fiber. Полный план из 25 уроков — на странице курса, микроблог поднимался с первого урока. Домашка ниже — выпускной экзамен: семь заданий, которые соберут курс в одну приёмку.
Карточки: проект «микроблог»
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 на уровне, который встречается нечасто. Для полного приложения добавьте HTTP-слой из курсов Gin или Echo, Docker из их уроков деплоя — и проект станет резюме-кейсом «от схемы до продакшена».
SQLite или Postgres для этого проекта?
Разрабатывать можно на SQLite из двадцать третьего урока — скорость контура тестов важнее. Деплоить — на Postgres: диалектные вещи вроде частичных индексов из двадцатого урока гоняйте на настоящем Postgres в CI. ent мультибазовый с седьмого урока: меняется DSN и драйвер, схема не меняется.
Как добавить роли и права к блогу?
Privacy-политики из девятнадцатого урока поверх клиента: viewer-контекст, DenyIfNoViewer, AllowIfAdmin — сервис и хендлеры менять не придётся, правила уже исполняются на каждом запросе и мутации. Транспортный слой (JWT, middleware) подставляет viewer в контекст — это материал курсов Gin и Echo.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
go-kit: микросервисный тулкит
Курс go-kit: endpoint, транспорт, логирование, метрики, трейсинг, circuit breaker, service discovery. 25 уроков с итоговым микросервисом.
- Понимать архитектуру endpoint → transport → service
- Оборачивать сервисы middleware: логи, метрики, трейсинг
- Подключать HTTP и gRPC транспорты
- Ставить rate limiting и circuit breaker