Содержание урока · 11 разделов
- ent update: две формы одной операции
- Set-мутаторы изнутри: мутация, валидация, UpdateDefault
- Массовое обновление: n как результат и как ответственность
- Возвращение обновлённого: как ent добывает строку
- Add и гонки: арифметика на стороне базы
- Upsert ent: что осталось за восьмым уроком
- Словарь обновления: мутаторы и их SQL
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Маршрут урока — путь одной записи от «прочитали» до «изменили». Первый сюжет — две формы обновления: точечная UpdateOneID по первичному ключу и массовая Update().Where() по предикатам из десятого урока. Второй — Set-мутаторы изнутри: что попадает в mutation, где живёт валидация и зачем UpdateDefault пересчитывается сам. Третий — семья Add/Clear/SetNillable: инкременты на стороне SQL и работа с Optional-полями четвёртого урока. Четвёртый — что именно возвращают Save у обеих форм: сущность или число, и как ent добывает обновлённые значения. Пятый — гонки: почему SQL-выражение age = age + 1 сильнее связки «прочитал, прибавил, записал», и как это продолжить на денормализованном счётчике из двенадцатого урока. Шестой — upsert-остаток восьмого урока: bulk-upsert через CreateBulk и OnConflict. Данные прежние: анна (26 лет, админ, два поста — «про рёбра» и «черновик»), борис (19), вера (17), группа go-ninja.
ent update: две формы одной операции#
Обновление в ent — это отдельный сгенерированный билдер, а не переклейка флагов на query: в blog/ent/user_update.go живут два строения. UserUpdate — массовая форма: client.User.Update() возвращает её без аргументов, Where принимает предикаты, Save отвечает парой (n int, err) — числом затронутых строк. UserUpdateOne — точечная форма: client.User.UpdateOneID(id) или UpdateOne(u) по готовой сущности; Where у неё нет — ключ и есть условие, а Save возвращает (*User, error). Это главный разворот ent update: выбор формы — выбор контракта возврата, и перепутать их не даст компилятор, потому что типы результата разные. Обе формы собирают мутацию одинаково: Set-методы копятся в поле UserMutation, и исполняется всё только на Save — до него билдер безопасно передаётся между функциями, клонируется и дополняется. Забудете Where в массовой форме — не будет ни ошибки, ни предупреждения: WHERE просто не сгенерируется, и UPDATE перепишет всю таблицу. ent не страхует от широких обновлений — это осознанный контракт SQL, и единственная страховка здесь ваша: сначала предикат, потом Set, и ни строкой иначе. Если вы искали по запросу ent update или «ent обновление записи» — запомните эту вилку первой: по id с возвратом строки, по условию с возвратом числа.
UpdateOneID: точечно, с возвратом сущности
GO// анна поправила профиль: точечное обновление по ключу
u, err := client.User.
UpdateOneID(1). // id анны
SetName("Анна").
SetAge(27).
Save(ctx)
if ent.IsNotFound(err) {
// такого id нет: *NotFoundError, а не паника
return err
}
if err != nil {
return err // соединение, валидация, отмена контекста
}
// u — обновлённая сущность со свежими значениями:
fmt.Println(u.Name, u.Age) // Анна 27UpdateOneID(1) принимает первичный ключ и возвращает UserUpdateOne; SetName и SetAge пишут значения в мутацию, Save исполняет UPDATE и возвращает обновлённую строку. Две ошибки различаются типом: ent.IsNotFound ловит «пользователя с таким id нет» — ent увидел ноль затронутых строк и вернул *NotFoundError, — а вторая проверка принимает всё остальное. Никакого «сначала Get, потом Update» здесь не нужно: одна операция, один поход в базу.
Проверка: что вернёт Save для отсутствующего id
client.User.UpdateOneID(999).SetName("x").Save(ctx) — пользователя с id 999 не существует. Что произойдёт?
Set-мутаторы изнутри: мутация, валидация, UpdateDefault#
Откроем user_update.go и посмотрим, что делает SetName: он пишет значение в m.name и помечает имя как изменённое — u.mutation.SetName(s). Всё остальное — работа Save: сгенерированный код собирает список изменённых полей из мутации, прогоняет их через валидаторы схемы и только затем строит SQL. Это значит, что валидация из третьего урока работает и на обновлении: MaxLen, Positive, NotEmpty — те же проверки, те же ValidationError до похода в базу. Правило старых значений стоит проговорить явно: SetX всегда перезаписывает поле целиком, независимо от того, что лежало в колонке, и ent не читает старое значение, чтобы «слить» его с новым — обновление это не патч в JavaScript-смысле, а замена. Отдельная магия — поля с UpdateDefault из третьего урока: если у вас в схеме updated_at объявлен с UpdateDefault(time.Now), ent сам добавит его в UPDATE при каждом сохранении, и вы не увидите этого ни в мутации, ни в списке изменённых полей — генератор вставляет дефолт на этапе сборки стейтмента. Побочный эффект той же механики: поля с Immutable из третьего урока в update-билдере отсутствуют как класс — Set-мутатора для них не сгенерировано, и компилятор не даст «случайно» переписать email-ключ или дату создания. Схема дисциплинирует запись ещё до базы.
Семья Add, Clear и SetNillable
GO// Optional-поля из четвёртого урока и инкременты:
u, err := client.User.
UpdateOneID(1).
AddAge(1). // SQL: age = age + 1
SetNillableBio(&bio). // *string: пишет только если не nil
Save(ctx)
// стереть сайт — NULL в nullable-колонке:
_, err = client.User.
UpdateOneID(1).
ClearWebsite(). // website = NULL
Save(ctx)
// массово почистить пустые био:
n, err := client.User.
Update().
Where(user.BioIsNil()).
SetBio("участник go-ninja").
Save(ctx) // n — сколько строк получило биоAddAge(n) генерирует не «прочитай и запиши», а выражение age = age + n прямо в SET — об этом отдельный сюжет ниже. SetNillableBio принимает *string: nil-указатель означает «поле не трогать», значение — записать; это удобная форма для обработчиков, где параметр пришёл из JSON и может отсутствовать. ClearWebsite ставит NULL — легально, потому что website Optional с четвёртого урока; ClearX для обязательных полей не сгенерирован, компилятор не пустит. BioIsNil — предикат из каталога десятого урока, он же отлично режет mass-update.
Сколько строк тронет массовое обновление
Прочитайте код и выберите, что он выведет:
// анна 26 лет, борис 19, вера 17.
n, err := client.User.Update().
Where(user.AgeGTE(18)).
SetBio("взрослый участник").
Save(ctx)
// чему равно n и err?Массовое обновление: n как результат и как ответственность#
Форма Update().Where().SetX().Save(ctx) — рабочая лошадь миграций, административных скриптов и «пометить всех прочитанными». Её контракт: Save возвращает n — число строк, которые база сообщила как затронутые, — и никогда не возвращает сущности: строки не прочитаны, а переписаны на месте. Из этого следуют три привычки. Первая: n = 0 — не ошибка, а данные «под условие никто не подошёл»; обрабатывайте как нормальный исход, а не как сбой. Вторая: после массового обновления в памяти приложения лежат устаревшие объекты — если страница должна показать новое состояние, перечитайте выборку Query-ем из девятого урока или возвращайте клиенту n, а не данные. Третья — про ответственность: WHERE без ограничений или забытый вовсе переписывает таблицу целиком, и ent оставляет это на совести автора. Практическая страховка — тестировать предикат сначала чтением: тот же Where на Query().Count(ctx) покажет, сколько строк «в зоне поражения», и уже потом тот же предикат уезжает в Update. Дорогие вычисления в SET — выражения, подзапросы — ent прячет за sql.Expr и Modify из седьмого урока, но в девяноста процентах случаев хватает Set-мутаторов и семьи Add/Clear. Массовая форма — это SQL-мышление: одна команда, один поход, число как отчёт.
Массовое обновление с проверкой прицела
GO// прицел: сколько строк под условием?
targets, err := client.User.Query().
Where(user.And(user.AgeGTE(18), user.BioIsNil())).
Count(ctx)
if err != nil {
return err
}
// тот же предикат — в обновление:
n, err := client.User.Update().
Where(user.And(user.AgeGTE(18), user.BioIsNil())).
SetBio("взрослый участник").
Save(ctx)
fmt.Println(targets, n) // 2 2 — прицел совпал с отчётомОдин и тот же предикат работает в Query и в Update — предикаты ent комбинируются через user.And из десятого урока и переиспользуются как константы. Count перед Update — не двойная работа, а дешёвая страховка на прод-данных: расхождение прицела и отчёта между двумя запросами означает, что данные менялись под вами, и это тоже полезный сигнал. n снова два: анна и борис, у веры био уже не пустое условие BioIsNil отсекло бы её даже по возрасту.
Возвращение обновлённого: как ent добывает строку#
Откуда берётся *User из UpdateOneID, если UPDATE в SQL традиционно не возвращает строк? Сгенерированный Save строит UPDATE с клаузой RETURNING на диалектах, которые умеют возвращать — PostgreSQL и современные SQLite, — и сканирует результат прямо в структуру User. Там, где RETURNING нет, ent делает честную связку: UPDATE, затем SELECT по тому же первичному ключу — два похода, но один вызов API. Для вас разницы нет ни в типах, ни в порядке проверки ошибок, есть разница в SQL-следе, и вы увидите её в логе драйвера. Следствия практические. Первое: u.UpdatedAt и другие поля с UpdateDefault в возвращённой сущности уже свежие — не нужно перечитывать, чтобы узнать время изменения. Второе: рёбра возвращённой сущности пусты — Edges.Groups будет nil, потому что UPDATE не возит соседей; захотите граф — With-вызовы из одиннадцатого урока на отдельном Query. Третье: ExecX и SaveX — паникующие близнецы Save — существуют и здесь, но в сервисном коде держитесь ошибки: потеря пользователя между проверкой и записью — нормальный параллельный мир, а не исключительный случай. И четвёртое: массовая форма RETURNING не использует в принципе — ей нечего сканировать, её отчёт уже описан числом n.
Обновили — получили — отдали
GO// HTTP-обработчик PATCH /users/:id (упрощённо):
u, err := client.User.
UpdateOneID(id).
SetName(in.Name). // из запроса
Save(ctx)
if ent.IsNotFound(err) {
http.NotFound(w, r) // 404: строки нет
return
}
if err != nil {
http.Error(w, err.Error(), 500) // соединение/валидация
return
}
// u.Name уже новое, u.UpdatedAt уже двигался UpdateDefault-ом:
renderUser(w, u) // рёбра пусты — если нужен граф, отдельный QueryПорядок веток не случайный: сначала NotFound как бизнес-исход (404), потом всё остальное как сбой (500). Рендерим сущность, которую вернул Save, — без повторного Get; свежие значения гарантированы возвратом. Если обработчику нужен граф пользователя — группы, посты — это отдельный Query с With-цепочкой из одиннадцатого урока, обновление и загрузка соседи не смешиваются.
Add и гонки: арифметика на стороне базы#
Самая коварная ошибка обновления — счётчик через чтение. Классический сценарий: прочитали пользователя, прибавили к age единицу в Go, записали SetAge(u.Age + 1). Между чтением и записью другой воркер успел сделать то же самое — и одна из двух единиц исчезла: оба UPDATE записали значение, вычисленное из одного и того же старого. AddAge(1) решает это на уровне SQL: в SET уезжает выражение age = age + 1, а не литерал — база применяет его атомарно к текущему значению строки, и сколько бы воркеров ни сходилось, каждая единица доезжает. Тот же принцип обслуживает лайки, балансы, номера ревизий и денормализованный счётчик posts_count из двенадцатого урока: поддерживать его надо Add-мутаторами и в хуках, а не связкой «прочитал-пересчитал». Оговорка честности: атомарность выражения не отменяет вопросов конкурентности вокруг других полей той же строки — если операция «прибавить и тут же прочитать результат» должна быть строго согласованной, это территория транзакций, до которых мы дойдём в шестнадцатом уроке. А если валидация схемы требует, чтобы возраст оставался неотрицательным, — проверяйте условие до Add или в хуке, потому что валидаторы ent видят прибавляемое число, а не результат выражения в базе.
Счётчик постов: продолжение прошлого урока
GO// поле posts_count из двенадцатого урока (денормализация):
// сортировка лент по нему была Order(ent.Desc(user.FieldPostsCount)).
// анна опубликовала пост — счётчик атомарно +1:
_, err := client.User.
UpdateOneID(anna.ID).
AddPostsCount(1).
Save(ctx)
// пост удалён — и минус:
_, err = client.User.
UpdateOneID(anna.ID).
AddPostsCount(-1).
Save(ctx)
// SQL в обоих случаях: UPDATE users SET posts_count = posts_count + $1
// параллельные публикации не теряютсяAddPostsCount(1) и AddPostsCount(-1) — один и тот же механизм выражения, знак в аргументе. Это ровно та дисциплина поддержки денормализации, которую просил двенадцатый урок: счётчик двигается в тех же точках кода, где создаются и удаляются посты — руками здесь, автоматом в хуках семнадцатого. Сортировка Order(ent.Desc(user.FieldPostsCount)) из прошлого урока читает уже честное число, без COUNT(*) на каждый запрос ленты.
Проверка: почему Add, а не Set
Два воркера одновременно делают u, _ := Get(ctx, id); client.User.UpdateOneID(id).SetAge(u.Age + 1).Save(ctx) при u.Age = 26. Что будет с age после обоих?
Upsert ent: что осталось за восьмым уроком#
Восьмой урок разобрал upsert на одиночной вставке: OnConflict на create-билдере, смена типа цепочки на UserUpsertOne, UpdateNewValues, Ignore, DoNothing, свой SET через Update(func(u ent.UserUpsert)) и возврат id через ID(ctx). Здесь закроем то, что осталось за кадром, — bulk-upsert: пачка строк одним INSERT-ом с общей оговоркой ON CONFLICT. Билдер CreateBulk из восьмого урока сам не умеет конфликтовать — но у него есть тот же OnConflict, и после него цепочка превращается в bulk-версию upsert: один стейтмент «вставь эти пять, а по конфликту на email обнови». Флаг sql/upsert включался ещё в седьмом уроке — проверьте features перед go generate, иначе метода просто не будет. Семантика такая же, как у одиночной: UpdateNewValues берёт mutable-поля из вставки, Immutable-поля вроде created_at не переписываются, а Update с UserUpsert позволяет свой SET поверх — например, прибавить возраст инкрементом к существующему значению. Полезно помнить и про упор MySQL: цель конфликта ему не нужна (ON DUPLICATE KEY не принимает колонок), PostgreSQL без цели откажется — это было в predict восьмого урока. Upsert ent — единственный штатный способ «синхронизации каталога», когда клиент присылает полный список сущностей, а база должна заменить состояние без дубликатов и без двух фаз.
Bulk-upsert: пачка одним стейтментом
GO// список пришёл от клиента; email — ключ синхронизации:
bs := make([]*ent.UserCreate, 0, 3)
for _, in := range incoming {
b := client.User.Create().
SetEmail(in.Email).
SetName(in.Name).
SetAge(in.Age)
bs = append(bs, b)
}
// вставить всё, по конфликту на email — обновить из вставки,
// а возраст прибавить к существующему:
err := client.User.
CreateBulk(bs...).
OnConflictColumns(user.FieldEmail).
UpdateNewValues().
Update(func(u *ent.UserUpsert) {
u.AddAge(1) // на каждом конфликте: age = age + 1
}).
Exec(ctx)CreateBulk собирает одиночные create-билдеры в один INSERT; OnConflictColumns(user.FieldEmail) задаёт цель конфликта — так же, как в восьмом уроке; UpdateNewValues обновляет mutable-поля значениями из вставки. AddAge(1) внутри Update — инкремент в upsert: для новых строк возраст придёт из вставки, для конфликтующих прибавится к текущему. Итог — один поход в базу на весь каталог: без SELECT-ов, без цикла Save, без двойных фаз.
Проверка: семантика bulk-upsert
В базе анна с email anna@entgo.io и возрастом 26. Bulk-upsert вставляет двух пользователей, один из них — анна с SetAge(27) и UpdateNewValues().Update(u.AddAge(1)). Что станет с возрастом анны?
Соберите оба маршрута обновления
Впишите точечный билдер, инкрементный мутатор и предикат «сайт пуст» для массовой формы.
Путь одного UpdateOneID
Соберите жизненный цикл точечного обновления: что происходит и в каком порядке от вызова Set до готовой сущности.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Словарь обновления: мутаторы и их SQL#
| Вызов | Форма | SQL в SET / поведение | Что вернуть |
|---|---|---|---|
| SetName("Анна") | точечная и массовая | name = 'Анна' — полная замена | замена значения целиком |
| SetNillableBio(&b) | точечная и массовая | bio = '…' или пропуск при nil | JSON-опциональный параметр |
| ClearWebsite() | точечная и массовая | website = NULL (поле Optional) | ошибки компиляции для Required |
| AddAge(1) | точечная и массовая | age = age + 1 — выражение | атомарный инкремент, без гонок |
| Where(user.BioIsNil()) | только массовая | WHERE bio IS NULL | предикаты из десятого урока |
| Update().Save(ctx) | массовая | UPDATE без предиката — вся таблица | осторожно: страхуйте Count-ом |
| OnConflictColumns(user.FieldEmail) | create + upsert | цель конфликта для PostgreSQL | MySQL цели не требует |
| UpdateNewValues() | upsert | SET из вставки, кроме Immutable | политика «есть — обнови» |
| Update(func(u *ent.UserUpsert)) | upsert | свой SET поверх политики | SetX/AddX/ClearX словарь |
| CreateBulk(bs...).OnConflict(...) | bulk-upsert | один INSERT ... ON CONFLICT | синхронизация каталога |
Runner-ов в курсе ent нет, и песочница не появится и здесь: обновлениям нужны настоящий драйвер и база — правило всего курса. Возьмите базу из домашек восьмого урока, включите лог SQL драйвера и исполните каждую форму из урока. Смотрите в лог три вещи: SET-часть UPDATE (литерал или выражение age = age + 1), WHERE-часть массовой формы (совпала ли с прицелом Count) и RETURNING в следе UpdateOneID. Затем умышленно забудьте Where в массовом обновлении на копии базы и посчитайте жертвы — этот шрам полезнее любого конспекта.
Что дальше#
Вы умеете менять данные обеими формами: точечная UpdateOneID возвращает обновлённую сущность с свежим updated_at, массовая Update().Where() отвечает числом и страхуется прицелом; Add даёт атомарные инкременты, Clear и SetNillable наводят порядок в Optional-полях четвёртого урока, а bulk-upsert довёл до конца тему восьмого. Оставшийся жест — прощание: двенадцатый урок закрыл чтение, тринадцатый — запись, а четырнадцатый урок 14-delete разберёт удаление во всех видах: DeleteOneID и Delete с предикатами, счёт удалённых строк, каскадное поведение рёбер из пятого урока и мягкое удаление своими руками. До встречи с ним — домашнее задание: переведите PATCH-обработчик из шаблона «Get, потом Update» на UpdateOneID и сравните число запросов в логе. Полный план из 25 уроков — на странице курса, каталог предикатов для Where — в десятом уроке, страницы и сортировка для списков — в двенадцатом.
Карточки: обновление и upsert
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- ent update — два сгенерированных билдера: UpdateOneID для точечной записи (Save → (*User, error), NotFoundError на пустом ключе) и Update().Where() для массовой (Save → (int, error), n = число затронутых строк). Выбор формы — выбор контракта возврата, компилятор не даст перепутать.
- Set-мутаторы пишут в мутацию и помечают поля; на Save поля валидируются по схеме третьего урока до похода в базу. UpdateDefault-поля вроде updated_at двигаются сами; Immutable-поля не имеют Set-мутаторов и в обновлении недостижимы.
- Семья Add/Clear/SetNillable: AddX(n) даёт атомарное выражение age = age + n без потерянных модификаций; ClearX ставит NULL и есть только у Optional-полей; SetNillableX принимает указатель и пишет только не-nil.
- Массовое обновление отвечает числом и не возвращает сущности: n = 0 — данные, не сбой; память после него устарела. Страховка — один и тот же предикат сначала в Query().Count, потом в Update.
- UpdateOneID добывает обновлённую строку через RETURNING (PostgreSQL, свежие SQLite) либо UPDATE + SELECT; UpdatedAt свежий, рёбра пусты — граф грузится отдельным Query с With.
- Инкременты счётчиков — только Add: SetAge(u.Age+1) из параллельных воркеров теряет единицы, выражение в SET — никогда. Денормализованный posts_count из двенадцатого урока поддерживается AddPostsCount(±1) в точках создания и удаления постов.
- Bulk-upsert закрывает тему восьмого урока: CreateBulk + OnConflictColumns + UpdateNewValues — один стейтмент на каталог; Update(func(u *ent.UserUpsert)) добавляет свой SET поверх, вплоть до инкремента. PostgreSQL требует цель конфликта, MySQL — нет.
- До похода в базу билдеры безопасны: их можно передавать, клонировать и дополнять; исполняется всё на Save. Формы без Where и с Where — разные типы, и компилятор ловит перепутанные контракты раньше базы.
Домашнее задание
Работайте на базе из восьмого урока — анна, борис, вера, их посты и группа go-ninja — и включите лог SQL драйвера. Первое: переведите PATCH-обработчик профиля с шаблона «Get, потом SetAge» на UpdateOneID и проверьте в логе, что уехал один UPDATE с RETURNING (или UPDATE + SELECT) вместо двух запросов; обработайте ent.IsNotFound как 404. Второе: массово поставьте био всем совершеннолетним без био — сначала прицел через Count с тем же Where, потом Update, и сверьте два числа; затем уберите Where и повторите на копии базы, чтобы увидеть цену забытого предиката. Третье: соберите счётчик постов из двенадцатого урока — завидите поле posts_count, двигайте его AddPostsCount(1) и AddPostsCount(-1) вокруг создания и удаления поста и запустите два горутины с сотней публикаций: после синхронизации счётчик обязан совпасть с числом постов, при Set-варианте разъедется. Четвёртое: почистите Optional-поля — ClearWebsite и SetNillableBio с nil-ом и значением — и посмотрите в лог разницу SQL. Пятое: соберите bulk-upsert каталога из трёх пользователей с конфликтом по email, UpdateNewValues и AddAge(1) в Update, прогоните дважды и посчитайте возраст анны: после второго прогона он должен вырасти ещё на единицу относительно вставки. Вопрос на подумать: почему валидаторы ent не проверят результат выражения age = age + 1, и в каких местах схемы это делает Add небезопасным?
Личные заметкиПусто
Вопросы и ответы по уроку
Чем UpdateOneID отличается от Get + UpdateOne?
UpdateOneID(id) сам знает ключ и уезжает одним UPDATE (с RETURNING или последующим SELECT) — одна операция, один поход в базу. Связка Get(ctx, id) + UpdateOne(u) — два запроса и окно для гонки: строка может исчезнуть между чтением и записью, и UpdateOne ответит NotFoundError на устаревшем объекте. UpdateOne удобен, когда id добывается из самой сущности цикла; для обработчиков по параметру маршрута берите UpdateOneID.
Как после массового обновления получить обновлённые сущности?
Отдельным Query с тем же предикатом: mass-update строки не читал, поэтому его честный результат — число. ent не вернёт срез «затронутых» — RETURNING у массовой формы не используется в принципе. Если сущности уже лежат в памяти, помните, что они устарели в момент Save: перечитайте их тем же Where из десятого урока, при необходимости — с With-цепочкой из одиннадцатого.
upsert или «SELECT, потом INSERT или UPDATE» — что выбирать?
Для сценариев «нет — вставь, есть — обнови» upsert из восьмого урока дешевле и безопаснее: один стейтмент, атомарность на стороне базы, нет окна между проверкой и записью. Связка SELECT + ветвление жива там, где решение о вставке зависит от внешней логики — отправки письма, проверки прав, — либо где диалект не поддерживает нужную форму ON CONFLICT. Для массовой синхронизации каталога выбора почти нет: CreateBulk с OnConflict — единственная форма без N запросов.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
go-kit: микросервисный тулкит
Курс go-kit: endpoint, транспорт, логирование, метрики, трейсинг, circuit breaker, service discovery. 25 уроков с итоговым микросервисом.
- Понимать архитектуру endpoint → transport → service
- Оборачивать сервисы middleware: логи, метрики, трейсинг
- Подключать HTTP и gRPC транспорты
- Ставить rate limiting и circuit breaker