Библиотека Go (Golang) разработчика
Kanalga Telegram’da o‘tish
Полезные материалы по всему, что может быть полезно Golang разработчику. По всем вопросам @evgenycarter
Ko'proq ko'rsatish2 691
Obunachilar
-124 soatlar
Ma'lumot yo'q7 kunlar
-1030 kunlar
Postlar arxiv
Разработчик приходит в Go из Java, Python или C# — и часто приносит с собой лишние слои, интерфейсы ради интерфейсов и сложную архитектуру там, где язык требует простоты.
🗓 20 июля в 20:00 МСК приглашаем вас на открытый урок, где мы разберём, как перестроить мышление под философию Go и писать код, который проще читать, сопровождать и защищать на проверке.
❗️На занятии поговорим об интерфейсах, ссылочных типах, строках, указателях, областях видимости, слайсах и мапах.
🔴 Открытый урок проходит в преддверии старта курса «Go-разработчик. Продвинутый уровень».
➡️ Зарегистрируйтесь, чтобы увидеть практический подход к Go без лишних абстракций: https://vk.cc/cZxhMh
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
⚙️ Go Runtime изнутри: GMP, GC, escape analysis и memory model
Почему Go "просто работает" быстро без ручного управления потоками — разбираем механику под капотом.
1. GMP-модель планировщика
Три сущности:
- G (Goroutine) — сама горутина: стек (растёт от 2KB), инструкция, статус
- M (Machine) — реальный OS-поток, который выполняет код
- P (Processor) — логический процессор, держит локальную очередь горутин (runqueue) и служит "разрешением" на выполнение
Ключевая идея:
GOMAXPROCS задаёт число P, а не M. M может блокироваться на syscall — тогда P отвязывается от него и находит себе другой свободный/новый M. Это и есть секрет: блокирующий syscall не останавливает остальные горутины.
Work stealing: если у P опустела локальная очередь, он крадёт горутины у других P (обычно половину батча) или лезет в глобальную очередь. Это балансировка без централизованного шедулера.
Preemption: до Go 1.14 планировщик был кооперативным — горутина без вызовов функций могла зависнуть навечно в tight loop. С 1.14 добавлен асинхронный preemption через сигналы (SIGURG), горутину прерывают принудительно.
2. Escape Analysis
Компилятор решает на этапе компиляции — стек или куча:
func onStack() int {
x := 42
return x // не убегает — на стеке
}
func onHeap() *int {
x := 42
return &x // адрес утекает — уходит в кучу
}
Проверить реальность:
go build -gcflags="-m" main.go
Частые причины "утечки" на кучу: возврат указателя, передача в интерфейс, замыкание, которое переживает функцию, слишком большой объект (компилятор консервативен).
3. GC: Concurrent Mark & Sweep
Go использует tri-color mark-and-sweep с write barrier, работающий конкурентно с мутатором (вашей программой):
- White — потенциальный мусор
- Grey — найден, но дети не просканированы
- Black — жив, обработан полностью
Write barrier ловит запись указателя во время фазы маркировки, чтобы не потерять объект, если мутатор переставляет ссылки прямо во время сборки (проблема "затирания" грей-объекта).
STW (Stop-The-World) случается только дважды за цикл, и оба раза — микросекунды: старт (включить write barrier) и финиш (выключить, финализировать).
Триггер GC — GOGC (по умолчанию 100%: сборка запускается, когда куча выросла вдвое с прошлого цикла) и с Go 1.19 — GOMEMLIMIT для soft memory limit.
4. Memory Model: happens-before
Go memory model формально описывает, при каких условиях запись в одной горутине гарантированно видна чтению в другой. Без синхронизации — никаких гарантий, компилятор и процессор вправе переупорядочить операции.
Гарантии happens-before дают:
// 1. Channel
ch := make(chan int)
go func() {
data = 42 // (A)
ch <- 1 // (B) happens-before получение
}()
<-ch // (C)
_ = data // видит 42, т.к. A → B → C
// 2. Mutex
mu.Lock()
// критическая секция
mu.Unlock() // Unlock happens-before следующий Lock
// 3. sync.Once
var once sync.Once
once.Do(f) // f гарантированно выполнится один раз, видимо всем
Без синхронизации — это data race, даже если "по факту не ломается" на вашей машине. Проверяйте:
go run -race main.go
Гонка данных в Go — undefined behavior на уровне спецификации, а не просто "риск получить неверное значение".
GMP даёт дешёвую конкурентность, escape analysis решает, где жить переменной, GC работает конкурентно и почти без пауз, а memory model — это контракт, без соблюдения которого все остальные гарантии бессмысленны.
#golang #runtime #gc #scheduler #concurrency
📲 Мы в MAX
👉 @golang_lib🕳
context.Context: Хватит превращать контекст в мусорное ведро
Мы передаем ctx context.Context первым аргументом почти в каждую функцию. Это кровеносная система Go-приложений, которая отлично справляется с отменой операций и таймаутами.
Но есть в интерфейсе контекста один метод, который открывает портал в ад - это Value().
Часто разработчики (особенно выходцы из языков с thread-local storage) смотрят на ctx.Value и думают: "О, отличная глобальная мапа! Положу-ка я сюда инстанс базы данных, логгер и данные пользователя, чтобы не прокидывать их через аргументы 10 функций".
Давайте разберем, почему это архитектурное преступление.
❌ Проблема 1: Убийство статической типизации
Сила Go - в строгой типизации на этапе компиляции. Когда вы кладете что-то в контекст, оно превращается в any (или interface{}).
// Где-то в мидлваре
ctx = context.WithValue(ctx, "db", dbConnection)
// Где-то в репозитории
db := ctx.Value("db").(*sql.DB) // Молимся, чтобы там не было nil
Ваша функция теперь имеет скрытую зависимость. Глядя на сигнатуру func GetUser(ctx context.Context), невозможно понять, что для ее работы нужен коннект к БД. Узнаете вы об этом только в рантайме, когда словите panic: interface conversion.
❌ Проблема 2: Медленный поиск (O(N))
Контекст - это не map (хэш-таблица). Под капотом WithValue каждый раз создает новый узел, который ссылается на родительский контекст. Образуется связный список (дерево).
Когда вы вызываете ctx.Value("key"), Go берет текущий узел и проверяет ключ. Если не нашел — идет к родителю. И так до самого верха. Если ваш ключ лежит в самом начале цепочки из 20 мидлварей, поиск будет прочесывать память каждый раз, убивая кэш процессора.
Как использовать ctx.Value правильно?
Официальная документация гласит: "Используйте значения контекста только для данных, привязанных к области видимости запроса (request-scoped data)".
✅ Идеальные кандидаты для ctx.Value:
• TraceID / RequestID (для распределенного трейсинга).
• IP-адрес клиента.
• ID авторизованного пользователя (но не вся структура User с бизнес-логикой).
То есть данные, которые нужны инфраструктуре (логгеру, метрикам), но никак не влияют на бизнес-логику функции.
🔥 Защита от коллизий ключей
Никогда не используйте встроенные типы (например, string) в качестве ключей для WithValue. Если два разных пакета используют ключ "id", они перезапишут данные друг друга.
Всегда создавайте неэкспортируемый кастомный тип:
type contextKey string
const userIDKey contextKey = "user_id"
// Обертка для записи
func WithUserID(ctx context.Context, id int) context.Context {
return context.WithValue(ctx, userIDKey, id)
}
// Обертка для чтения (безопасная, возвращает (int, bool))
func UserIDFromContext(ctx context.Context) (int, bool) {
id, ok := ctx.Value(userIDKey).(int)
return id, ok
}
Про context.TODO():
Если вы пишете код и не знаете, откуда взять контекст (например, рефакторите старый легаси) - используйте context.TODO(). Технически это тот же context.Background(), но семантически это маячок для линтеров и коллег: "Я оставил здесь технический долг, позже нужно прокинуть нормальный контекст".
#golang #architecture #context #bestpractices #cleancode
📲 Мы в MAX
👉 @golang_libКуда уходит память? Разбираемся с Escape-анализом в Go 🚀
Многие любят Go за встроенный сборщик мусора (GC) и простоту работы с указателями. Но чтобы писать по-настоящему быстрый код, нужно понимать, где именно аллоцируется память: на стеке (stack) или в куче (heap).
Выделение памяти на стеке обходится практически бесплатно (это просто сдвиг указателя), а вот аллокации в куче нагружают GC и снижают общую производительность приложения.
Как компилятор Go решает, куда положить переменную? С помощью Escape-анализа.
Главное правило: если ссылка на переменную «убегает» (escapes) за пределы функции, где она была создана, переменная отправляется в кучу. Если нет - остается на быстром стеке.
Когда переменная почти наверняка «убегает» в кучу:
• Возврат указателя из функции (например,
return &MyStruct{}).
• Отправка указателя или структуры с указателями в канал (компилятор не знает, когда и в какой горутине получатель это прочитает).
• Присвоение значения в interface{}. Классический пример: вызов fmt.Println(myVar) отправляет myVar в кучу, так как функция под капотом принимает ...any.
• Размер переменной слишком велик для стека или неизвестен на этапе компиляции (например, слайс динамического размера, который мы инициализируем через переменные).
Как проверить свой код?
Запустите сборку с флагом -gcflags="-m":
go build -gcflags="-m" main.go
В консоли вы увидите строки вида escapes to heap - компилятор прямо расскажет, какие переменные переехали в кучу и почему.
💡 Практический совет:
Не используйте указатели слепо в надежде «избежать лишнего копирования». Очень часто передача небольшой структуры по значению (создание копии на быстром стеке) обходится гораздо дешевле, чем передача по указателю (аллокация в куче + последующая работа сборщика мусора).
#golang #memory #backend #оптимизация
📲 Мы в MAX
👉 @golang_lib👣 Многие разработчики приходят в Go с багажом паттернов из Java и C#. В результате простой и понятный код постепенно обрастает слоями абстракций, лишними интерфейсами и десятками DTO, которые усложняют поддержку проекта.
🗓 8 июля в 20:00 МСК приглашаем вас на открытый урок в преддверии старта курса «Go-разработчик. Продвинутый уровень». На занятии разберём, почему привычные подходы из других языков не всегда работают в Go, как выстроить архитектуру через Handler → Service → Repository без циклических зависимостей, где действительно нужны интерфейсы и как избежать избыточных абстракций.
❗️Вы узнаете, какие ошибки чаще всего встречаются на проверках кода, научитесь проектировать приложения с понятным разделением ответственности и поймёте, как писать код, который останется читаемым и через полгода.
➡️ Регистрируйтесь и познакомьтесь с подходом, который помогает писать на Go проще, надёжнее и профессиональнее: https://vk.cc/cZcTF2
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
🔀 Fan-Out / Fan-In: Строим конвейер, который не лопнет
Представьте задачу: у вас есть CSV-файл на 10 миллионов строк (или бесконечный стрим из Kafka). Каждую строку нужно прочитать, сходить с ней в тяжелый внешний API (парсинг/обогащение) и записать результат в базу.
• Решение джуна: Читать по одной строке, ходить в API, писать в БД. Очень надежно и очень медленно. Файл будет обрабатываться неделю.
• Решение мидла: На каждую строку делать
go func(). Через секунду мы откроем 10 миллионов горутин, забьем сеть, положим внешний API, исчерпаем файловые дескрипторы и умрем от OOM (Out Of Memory).
Нам нужен баланс: обрабатывать данные параллельно, но с жестким лимитом ресурсов. Встречайте паттерн Pipeline (Конвейер) с применением Fan-Out / Fan-In.
Что это такое?
• Fan-Out (Разветвление): Один канал генерирует задачи, а группа из N воркеров (фиксированный пул) читает из этого одного канала. Задачи распределяются между ними автоматически.
• Fan-In (Слияние): Воркеры пишут результаты в свои личные исходящие каналы, а специальная функция сливает эти N каналов в один итоговый поток.
Как это выглядит в коде (The Go Way):
// 1. Fan-Out: Воркер читает из in и пишет в свой out
func worker(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
// Имитируем тяжелую работу
time.Sleep(time.Millisecond * 100)
out <- n * 2
}
}()
return out
}
// 2. Fan-In: Сливаем каналы от всех воркеров в один
func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
// Функция, которая перекладывает данные из конкретного канала в общий
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
out <- n
}
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}
// Фоновая горутина закроет общий канал, когда все воркеры отработают
go func() {
wg.Wait()
close(out)
}()
return out
}
Собираем всё вместе:
func main() {
// Канал с задачами (генератор опустим для краткости)
in := generateTasks()
// Запускаем Fan-Out: создаем фиксированно 3 воркера
w1 := worker(in)
w2 := worker(in)
w3 := worker(in)
// Запускаем Fan-In: собираем результаты из 3 каналов в 1
for result := range merge(w1, w2, w3) {
fmt.Println(result)
}
}
🔥 Нюансы для Senior-ов:
1. Почему просто не писать всем воркерам в один общий канал?
Можно. Часто так и делают (называется Worker Pool). Но классический Fan-In (с функцией merge) дает гибкость: вы можете строить сложные графы обработки, где каналы передаются из функции в функцию, не завязываясь на глобальные состояния и мьютексы.
2. Утечки горутин (Goroutine Leaks).
В этом коде есть слабое место. Если цикл чтения итогового результата в main прервется досрочно (например, возникла ошибка и мы сделали return или break), воркеры зависнут навсегда, пытаясь записать данные в каналы, которые никто не читает.
Золотое правило: Всегда прокидывайте context.Context или канал done во все функции конвейера и проверяйте case <-ctx.Done(): внутри циклов for.
3. Порядок не гарантирован.
Fan-Out перемешивает данные. Если вам критически важно сохранить исходную последовательность строк CSV, этот паттерн нужно усложнять (например, передавать структуру с индексом и сортировать буфер на выходе).
Кто использует чистые каналы на проде, а кто перешел на готовые библиотеки вроде samber/lo (или errgroup)? Пишите в комменты 👇
#golang #concurrency #architecture #patterns #cleancode
📲 Мы в MAX
👉 @golang_lib🚀 Подборка полезных IT каналов в Max
Системное администрирование, DevOps 📌
https://max.ru/i_odmin Все для системного администратора
https://max.ru/bash_srv Bash Советы
https://max.ru/sysadminof Книги для админов, полезные материалы
https://max.ru/i_odmin_book Библиотека Системного Администратора
https://max.ru/i_devops DevOps: Пишем о Docker, Kubernetes и др.
https://max.ru/tipsysdmin Типичный Сисадмин
Excel лайфхак 📌
https://t.me/Excel_lifehack Excel лайфхак
Английский с нуля 🇬🇧
https://max.ru/UchuEnglish
1C разработка 📌
https://max.ru/odin1c_rus Cтатьи, курсы, советы, шаблоны кода 1С
Программирование C++📌
https://max.ru/cpp_lib Библиотека C/C++ разработчика
Программирование Go📌
https://max.ru/golang_lib Библиотека Go (Golang) разработчика
Программирование React📌
https://max.ru/react_lib React
Программирование Python 📌
https://max.ru/python_of Python академия.
https://max.ru/BookPython Библиотека Python разработчика
Java разработка 📌
https://max.ru/bookjava Библиотека Java разработчика
GitHub Сообщество 📌
https://max.ru/githublib Интересное из GitHub
Базы данных (Data Base) 📌
https://max.ru/database_info Все про базы данных
Фронтенд разработка 📌
https://max.ru/frontend_1 Подборки для frontend разработчиков
Библиотеки 📌
https://max.ru/programmist_of Книги по программированию
https://max.ru/proglb Библиотека программиста
https://max.ru/bfbook Книги для программистов
Программирование 📌
https://max.ru/bookflow Лекции, видеоуроки, доклады с IT конференций
https://max.ru/itmozg Программисты, дизайнеры, новости из мира IT
https://max.ru/php_lib Библиотека PHP программиста 👨🏼💻👩💻
Шутки программистов 📌
https://max.ru/itumor Шутки программистов
Защита, взлом, безопасность 📌
https://max.ru/thehaking Канал о кибербезопасности
https://max.ru/xakkep_1 Хакер Free
Книги, статьи для дизайнеров 📌
https://max.ru/odesigners Статьи, книги для дизайнеров
Математика 📌
https://max.ru/Pomatematike Канал по математике
https://max.ru/phismat_1 Обучающие видео, книги по Физике и Математике
Вакансии 📌
https://max.ru/progjob Вакансии в IT
Мир технологий 📌
https://max.ru/mir_teh Канал для любознательных
Бонус 📌
https://max.ru/piterspb_78 Свежие новости Санкт-Петербурга
https://max.ru/mockva_life Свежие новости Москвы
https://max.ru/piterspb Питер Новости: Санкт-Петербург / СПБ / ДТП
🔎
pprof: Как найти функцию, которая жрет 80% CPU
Сервис на проде внезапно упирается в полку по процессору.
Что делает новичок? Сидит и смотрит в исходники взглядом гипнотизера, пытаясь угадать: "Ну, наверное, это регулярка тормозит". Или обкладывает весь код вызовами time.Now() в начале и time.Since() в конце каждой функции.
Сеньор открывает терминал, пишет одну команду и через 30 секунд получает точное имя функции, номер строки и процент украденного CPU. Наш инструмент pprof.
Он встроен прямо в стандартную библиотеку Go. Работает по принципу семплирования: раз в 10 миллисекунд рантайм Go "замирает", смотрит на стеки всех запущенных горутин и записывает: "Так, в эту микросекунду процессор выполнял функцию json.Unmarshal.
Давайте проведем вскрытие за 4 шага.
Шаг 1. Включаем «жучка» в коде
Всё, что нужно сделать в вашем сервисе - импортировать один пакет со знаком подчеркивания:
package main
import (
"net/http"
_ "net/http/pprof" // <-- Подключаем магию
)
func main() {
// ... ваша основная бизнес-логика ...
// Вешаем pprof на отдельный внутренний порт!
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
}
Нюанс для Senior-ов: Магия _ "net/http/pprof" работает только с дефолтным http.DefaultServeMux. Если вы используете chi, gin или кастомный http.NewServeMux(), роуты профилировщика нужно регистрировать в ваш роутер руками (они лежат в пакете net/http/pprof как обычные хендлеры).
Шаг 2. Натравливаем профилировщик
Сервис крутится под нагрузкой. Открываем терминал на своей рабочей машине и говорим Go: *"Послушай этот сервер 30 секунд и собери мне статистику"*:
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
Терминал подумает полминуты и превратится в интерактивную консоль (pprof).
Шаг 3. Магия двух колонок
Внутри консоли пишем команду top10:
Showing nodes accounting for 2.45s, 88.1% of 2.78s total
flat flat% sum% cum cum%
1.82s 65.5% 65.5% 1.82s 65.5% regexp.(*bitState).reset
0.34s 12.2% 77.7% 0.45s 16.2% encoding/json.Unmarshal
0.15s 5.4% 83.1% 2.50s 89.9% main.processOrder
Главная ловушка новичка - смотреть на колонку cum.
• flat - сколько времени процессор провел исключительно внутри тела этой функции.
• cum (cumulative) - сколько времени процессор провел в этой функции + во всех функциях, которые она вызвала внутри себя.
Пример: У функции main.processOrder показатель cum равен 89.9%, но flat всего 5.4%. Это значит, что сама она почти ничего не считает, она просто вызвала тяжелую регулярку (regexp.reset, у которой flat аж 65.5%). Лечить нужно регулярку, а не processOrder!
Шаг 4. Визуальный экстаз (Flame Graph)
Смотреть в ASCII-таблицы в 2026 году больно. Выходим из консоли (Ctrl+D) и запускаем ту же команду, но добавив флаг -http:
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30
В браузере мгновенно откроется шикарный веб-интерфейс. Переключаем вид сверху в режим Flame Graph (Огненный граф):
1. Перед вами "столбы". Чем шире столб по горизонтали - тем больше CPU сожрала эта функция. 2. Кликаем мышкой на самый широкий красный столб.
2. Переключаемся во вкладку Source.
3. Видим наш исходный код, где подсвечена конкретная строчка var re = regexp.MustCompile(...), компилирующая регулярку внутри цикла в хендлере. Прод спасен.
🔥 Senior Warning: Кровавая цена дефолта
Никогда не выставляйте порт 6060 наружу в интернет!
По адресу /debug/pprof/goroutine?debug=2 любой школьник без авторизации скачает полный текстовый дамп всех запущенных горутин. В их стеках будут лежать ваши пароли от БД в сыром виде, Bearer-токены пользователей и приватные ключи.
В продакшене pprof обязан висеть строго на localhost, куда разработчик заходит через kubectl port-forward или SSH-туннель.
📲 Мы в MAX
👉 @golang_libConnection Pool: Как Go убивает базу данных (и как этого избежать)
Выкатываете вы новый сервис, запускаете нагрузочное тестирование, и тут логи начинают истекать кровью:
FATAL: sorry, too many clients already.
Вы бежите к админам (или в консоль AWS) и видите, что ваш скромный сервис на Go открыл 1500 соединений к PostgreSQL и положил базу. Почему так вышло?
Потому что по умолчанию стандартный пакет database/sql не имеет лимита на количество открытых соединений. Если к вам прилетит 1000 запросов одновременно, Go честно попытается открыть 1000 TCP-соединений. Для БД каждый коннект - это отдельный тяжелый процесс, который жрет память.
Чтобы не быть врагом своим девопсам, нужно всегда настраивать пул соединений. Это делается тремя магическими методами.
1. SetMaxOpenConns(n) - Ограничение жадности
Это жесткий лимит на количество одновременно открытых соединений к базе.
Если вы поставили лимит 50, а пришел 51-й запрос, горутина просто заблокируется и будет покорно ждать, пока кто-нибудь не освободит коннект (или пока не отвалится по таймауту контекста).
2. SetMaxIdleConns(n) - Пул в режиме ожидания
Сколько соединений держать открытыми, когда нет нагрузки?
Если поставить слишком мало, при скачке трафика Go начнет судорожно устанавливать новые TCP-соединения (это долгий хендшейк, потеря драгоценных миллисекунд).
Золотое правило: Часто MaxIdleConns ставят равным MaxOpenConns. Это гарантирует, что пул всегда прогрет и готов к бою.
3. SetConnMaxLifetime(d) - Защита от тухлых коннектов
Если коннект висит слишком долго, база данных, балансировщик (HAProxy) или файрвол могут закрыть его в одностороннем порядке. Go об этом не узнает. Когда приложение попытается послать запрос в такой коннект, вы получите классическую ошибку driver: bad connection.
Заставьте Go принудительно пересоздавать коннекты раз в час или несколько минут.
Как это выглядит в коде:
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatal(err)
}
// Задаем лимиты в зависимости от размера вашего инстанса БД
// и количества запущенных подов сервиса.
db.SetMaxOpenConns(50)
db.SetMaxIdleConns(50)
db.SetConnMaxLifetime(30 * time.Minute)
// Опционально: время жизни коннекта в простое (Go 1.15+)
db.SetConnMaxIdleTime(5 * time.Minute)
🔥 Senior Tip: PgBouncer
Если у вас 20 подов микросервиса, и каждый имеет MaxOpenConns = 50, суммарно в базу может прилететь 1000 коннектов. Postgres от такого станет плохо (оптимально для него - пара сотен коннектов).
Поэтому в мире взрослых нагрузок между Go и базой всегда ставят PgBouncer (в режиме transaction pooling). Он мультиплексирует тысячи легковесных клиентских коннектов в десятки реальных коннектов к базе. В таком случае в Go можно ставить лимиты побольше, а общую нагрузку будет сглаживать PgBouncer.
У кого базы падали в пятницу вечером из-за дефолтных настроек sql.DB? Поднимайте руки в комментах 👇
#golang #database #architecture #performance #bestpractices
📲 Мы в MAX
👉 @golang_lib🔌 Circuit Breaker: Как не добить лежачего (и не умереть самому)
Знакомая ситуация: внешний сервис (например, процессинг платежей или тяжелая аналитика) начинает тормозить. Ваши запросы к нему зависают по таймауту.
Джунское решение: "Наверное, сеть моргнула. Добавлю-ка я ретраи!"
Результат: внешний сервис и так лежит под нагрузкой, а ваши ретраи создают шторм запросов (Retry Storm), добивая его окончательно. Тем временем, в вашем сервисе копятся тысячи горутин, ожидающих ответа, исчерпываются коннекты к вашей собственной базе данных, случается OOM или паника. Поздравляю, вы получили каскадный сбой (Cascading Failure).
Чтобы этого избежать, в архитектуре микросервисов используется паттерн Circuit Breaker (Предохранитель).
Идея взята из электрики. Если в сети короткое замыкание - пробки выбивает, чтобы не сгорел весь дом.
Как это работает (Три состояния):
1. Closed (Закрыт): Всё хорошо. Запросы идут во внешний сервис как обычно. Если случаются ошибки, предохранитель увеличивает счетчик неудач.
2. Open (Открыт): Пробили лимит ошибок (например, 5 таймаутов подряд). "Пробки выбило". Теперь Circuit Breaker перехватывает все новые запросы и моментально возвращает ошибку, даже не пытаясь сходить по сети.
Профит: Мы экономим свои ресурсы (горутины не висят) и даем внешнему сервису время остыть и перезапуститься.
3. Half-Open (Полуоткрыт): Прошел таймаут (например, 10 секунд). Мы пропускаем один тестовый запрос, чтобы проверить "пульс" больного. Если запрос успешен - цепь закрывается (переходим в Closed). Если упал - снова Open.
Реализация на Go:
Не нужно писать конечные автоматы руками. В комьюнити есть стандарт де-факто - библиотека
sony/gobreaker (да, от той самой Sony).
import "github.com/sony/gobreaker"
var cb *gobreaker.CircuitBreaker
func init() {
settings := gobreaker.Settings{
Name: "BillingAPI",
MaxRequests: 1, // Сколько запросов пускать в состоянии Half-Open
Timeout: 10 * time.Second, // Сколько времени висеть в состоянии Open
ReadyToTrip: func(counts gobreaker.Counts) bool {
// Открываем цепь, если было 5 ошибок подряд
return counts.ConsecutiveFailures >= 5
},
}
cb = gobreaker.NewCircuitBreaker(settings)
}
func GetBalance(userID int) (float64, error) {
// Оборачиваем опасный сетевой вызов в cb.Execute
result, err := cb.Execute(func() (interface{}, error) {
return billingAPI.Fetch(userID) // Реальный поход в сеть
})
if err != nil {
// Если цепь открыта, cb.Execute сразу вернет gobreaker.ErrOpenState.
// Идеальное место, чтобы отдать закешированное значение (Graceful Degradation)!
if errors.Is(err, gobreaker.ErrOpenState) {
return getBalanceFromCache(userID)
}
return 0, err
}
return result.(float64), nil
}
🔥 Не суйте предохранители везде
Circuit Breaker нужен исключительно для интеграций с внешними сервисами или некритичными зависимостями. Если вы попытаетесь обернуть им запросы к вашей основной базе данных (PostgreSQL), вы просто замаскируете проблему. Если лежит ваша главная БД - сервис должен лечь вместе с ней, а не пытаться делать вид, что всё нормально.
#golang #architecture #microservices #circuitbreaker #systemdesign
📲 Мы в MAX
👉 @golang_lib📜 Паттерн Saga: Как откатить то, что откатить нельзя
Представьте классическую задачу: клиент нажимает кнопку «Купить тур». Вашему бэкенду нужно сделать три вещи:
1. Забронировать рейс (через API авиакомпании).
2. Забронировать отель (через микросервис отелей).
3. Списать деньги (через платежный шлюз).
В монолите с одной базой вы бы просто открыли транзакцию
BEGIN ... COMMIT и спали спокойно. Но у нас микросервисы. Вы не можете повесить лок на базу данных авиакомпании.
Что если рейс забронирован, отель подтвержден, а на карте клиента нет денег? Вы не можете просто сказать ROLLBACK. Рейс уже куплен. Вы потеряли деньги компании.
Здесь на сцену выходит Паттерн Saga.
Суть Саги: Распределенная транзакция разбивается на серию локальных. И самое главное: для каждого шага (Execute) вы обязаны написать шаг отмены (Compensate).
Если на третьем шаге происходит ошибка, Сага разворачивается и вызывает функции отмены для второго и первого шагов в обратном порядке.
В архитектуре есть два пути: Хореография (микросервисы кидаются событиями через Kafka) и Оркестрация (один сервис управляет всем). В Go элегантнее всего пишется Оркестратор.
Реализация Оркестратора на Go:
type SagaStep struct {
Name string
Execute func(ctx context.Context) error
Compensate func(ctx context.Context) error
}
func RunSaga(ctx context.Context, steps []SagaStep) error {
var completedSteps []SagaStep
// 1. Идем вперед
for _, step := range steps {
err := step.Execute(ctx)
if err == nil {
completedSteps = append(completedSteps, step)
continue
}
log.Printf("Ошибка на шаге %s: %v. Начинаем откат...", step.Name, err)
// 2. Ошибка! Идем назад и откатываем
for i := len(completedSteps) - 1; i >= 0; i-- {
rollbackStep := completedSteps[i]
if compErr := rollbackStep.Compensate(ctx); compErr != nil {
// Это катастрофа. Подробнее в Senior Tips.
log.Printf("КРИТИЧЕСКАЯ ОШИБКА отката %s: %v", rollbackStep.Name, compErr)
}
}
return fmt.Errorf("saga failed on step %s: %w", step.Name, err)
}
return nil
}
Как это выглядит при вызове:
steps := []SagaStep{
{
Name: "BookFlight",
Execute: func(ctx context.Context) error { return flightAPI.Book(userID) },
Compensate: func(ctx context.Context) error { return flightAPI.Cancel(userID) },
},
{
Name: "BookHotel",
Execute: func(ctx context.Context) error { return hotelAPI.Book(userID) },
Compensate: func(ctx context.Context) error { return hotelAPI.Cancel(userID) },
},
// ... и так далее
}
err := RunSaga(ctx, steps)
🔥 Нюансы:
1. Компенсации не имеют права падать. Если функция Execute упала, это нормально (нет денег на карте, сеть моргнула). Но если упала функция Compensate (не смогли отменить бронь отеля) - ваша система переходит в неконсистентное состояние. Отель ждет гостя, а клиент ничего не оплатил.
Решение: Компенсирующие действия должны отправляться в надежную очередь (Dead Letter Queue) и ретраиться до посинения (или до вмешательства саппорта).
2. Идемпотентность - царица Саги.
Так как функции компенсации могут ретраиться бесконечно, они обязаны быть идемпотентными (вспоминаем прошлый пост!). Если мы 5 раз вызовем hotelAPI.Cancel, бронь должна отмениться один раз, а остальные 4 вызова должны вернуть 200 OK.
3. Грязное чтение (Lack of Isolation).
Сага не обладает свойством "I" из ACID (Изоляция). Между бронированием отеля и списанием денег могут пройти секунды. В этот момент другой сервис может увидеть, что отель забронирован. Если сага откатится, другой сервис останется с неверными данными. Учитывайте это при проектировании (например, вводите статусы PENDING).
Сага - это сложно. Не тяните её в проект, пока можете обойтись одной таблицей в монолите. Но если уж распределили базу, будьте готовы платить за это кодом.
📲 Мы в MAX
👉 @golang_lib🔍Тестовое собеседование с Go Senior из Uzum в этот четверг
11 июня(в четверг!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle Go-разработчика.
Как это будет:
📂 Маруф Караев, Senior из Uzum, ex-Яндекс, ex-EPAM будет задавать реальные вопросы и задачи разработчику-добровольцу
📂 Маруф будет комментировать каждый ответ респондента, чтобы дать понять, чего от вас ожидает собеседующий на интервью
📂 В конце можно будет задать любой вопрос Маруфу
Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Go-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы.
Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_go_bot
Реклама.
О рекламодателе.
🔄 Идемпотентность: Как не списать деньги дважды при ретраях
Худшее, что может сделать ваш микросервис, это упасть с пятисоткой. Нет, вру. Худшее, это тихо выполнить операцию дважды.
Все мы знаем, что сеть ненадежна. Поэтому мы оборачиваем HTTP-клиенты в ретраи (например, через
hashicorp/go-retryablehttp). Но тут возникает классическая ловушка распределенных систем: Таймаут не означает, что запрос не выполнился.
Представьте: клиент отправляет запрос "Списать 1000 рублей". Сервис списывает деньги, отправляет ответ 200 OK, но по пути сеть моргнула, и клиент отвалился по таймауту. Клиент делает ретрай. Сервис списывает еще 1000 рублей. Поздравляю, вы в новостях.
Решение: Идемпотентность
Операция называется идемпотентной, если многократное её выполнение дает тот же результат, что и однократное.
Для GET или PUT запросов это работает из коробки. Для POST (создание ресурса, списание) нам нужен Idempotency-Key.
Как это работает на практике:
1. Мобилка (или вызывающий сервис) генерирует уникальный ID для конкретной бизнес-операции (например, UUID v4) и передает его в заголовке Idempotency-Key.
2. Наш Go-сервис перед началом работы проверяет этот ключ.
❌ Как делать НЕ надо (Redis-ловушка):
Многие новички сразу тянутся за Redis: записывают ключ туда, ставят TTL. Но что если Redis недоступен? Или ключи вытеснились из-за нехватки памяти (eviction)? Ваша консистентность рассыпается.
✅ Как надо (Атомарность БД):
Идемпотентность должна жить там же, где и состояние (state) операции — в вашей реляционной базе.
Используйте Unique Constraint в PostgreSQL:
func (s *Service) Charge(ctx context.Context, userID int, amount float64, idempotencyKey string) error {
// Пытаемся сохранить ключ в таблицу idempotency_keys
// Таблица имеет UNIQUE индекс по полю key
_, err := s.db.ExecContext(ctx, `
INSERT INTO idempotency_keys (key, status)
VALUES ($1, 'processing')
ON CONFLICT (key) DO NOTHING
`, idempotencyKey)
// Если ни одной строки не затронуто, значит ключ уже есть!
// Мы можем вернуть старый результат (или ошибку "Уже в обработке")
if err == nil && sqlResult.RowsAffected() == 0 {
return ErrAlreadyProcessed
}
// ... здесь выполняем списание ...
// Обновляем статус ключа на 'done' (в рамках транзакции списания)
return nil
}
🔥 Нюансы для Senior-ов:
• Срок жизни: Ключи идемпотентности не должны жить вечно. Обычно хватает 24-48 часов. Настройте фоновую джобу или партиционирование таблиц (Table Partitioning в PG) для старых ключей, чтобы таблица не пухла до терабайтов.
• Гонка ретраев (Thundering Herd): Если первый запрос завис, а клиент тут же послал второй (ретрай), второй запрос должен получить статус processing и подождать (или отвалиться с 409 Conflict), а не начинать работу параллельно.
Доверяйте базе данных больше, чем сети.
#golang #architecture #microservices #bestpractices #systemdesign
📲 Мы в MAX
👉 @golang_lib⚙️ Лучшие практики в Go: кейсы, которые особенно ждём
С 1 июня у нас стартует новый сезон Podlodka Go Crew — «Лучшие практики в Go». Он пройдёт при поддержке 2ГИС. Будем говорить о том, как писать код, который нормально живёт в продакшене при росте нагрузки и команды.
Вот какие сессии в этом сезоне мы ждём с особенным вниманием:
🚀 «Практика Go оптимизаций: растем вместе с нагрузкой», Алексей Акулович — путь сервиса от прототипа до миллионов RPS, оптимизация CPU, grpc, protobuf и даже собственный GC поверх гошного.
🏗 «Эволюция структуры Go-проекта: как 30 человек пушат в один репозиторий», Кирилл Возжеников — про рост продуктового монорепозитория, построение системы и практики, которые помогают не утонуть в хаосе.
🧩 «Как и зачем писать свой CDC на Go», Юра Саргсян — о ситуации, где стандартных решений уже недостаточно. Postgres, Kafka, гарантии доставки и и подводные камни логической репликации.
🔥 А в конце сезона проведём битву кейсов «50 оттенков межсервисного взаимодействия» — вместе разберём архитектурные задачи с метриками, схемами и ограничением на уточняющие вопросы.
👨💻 Будет много живых инженерных кейсов и обсуждений без абстракций. Приходите — будем разбирать реальные задачи и подходы, которые помогают строить надёжные системы.
🎟 Билеты здесь: https://podlodka.io/gocrew
📦
go mod: Хватит удалять go.sum, когда что-то сломалось
Управление зависимостями в Go выглядит элегантно: написал import, сделал go mod tidy, и всё работает. Но стоит случиться конфликту версий, как многие разработчики переходят в режим паники: удаляют go.mod, удаляют go.sum, чистят кэш и надеются на чудо.
Давайте разберем, как это работает под капотом, чтобы перестать воевать с тулчейном.
1. Миф про go.sum (Это не lock-файл!)
Выходцы из JS (NPM) или Python (Poetry) часто думают, что go.sum - это аналог package-lock.json. Это фатальная ошибка. В Go версия зависимости фиксируется строго в go.mod.
А что тогда делает go.sum? Это криптографическая база данных.
Когда вы скачиваете пакет впервые, Go считает хэш от его исходников и записывает в go.sum. При следующих сборках (например, в CI/CD) Go проверяет, совпадает ли скачанный код с этим хэшем.
Если хэш не совпадает, тулчейн бьет тревогу. Это защита от атаки Supply Chain (когда злоумышленник взломал GitHub-репозиторий популярной библиотеки и подменил там код, не меняя версию).
Вывод: Удаляя go.sum при любой непонятной ошибке, вы отключаете встроенный антивирус языка. Если хэш не сошелся - сначала выясните, почему, а не сносите файл.
2. Директива replace - заряженный пистолет
В go.mod есть мощная фича: replace. Она позволяет подменить одну зависимость на другую (или на локальную папку).
✅ Идеальный юзкейс: Вы разрабатываете два микросервиса локально. Сервис А зависит от Сервиса Б. Чтобы не пушить изменения Б в гит ради каждой проверки, вы пишете:
replace github.com/myorg/serviceB => ../serviceB
❌ Катастрофа: Вы забываете убрать эту строчку и мержите в main.
Если ваш код - это библиотека, которую скачают другие, их тулчейн проигнорирует ваш replace (он работает только в главном модуле). В итоге у них всё сломается. Если это финальный бинарник - CI/CD упадет, потому что пути ../serviceB на сборочном сервере не существует.
3. Проклятие GOPROXY
По умолчанию Go скачивает модули не напрямую с GitHub, а через прокси-сервер Google (proxy.golang.org).
Если вы работаете в энтерпрайзе с приватными репозиториями (GitLab, Bitbucket), go mod tidy вернет ошибку 404 или 410, потому что гугловский прокси не имеет доступа к вашему внутреннему коду.
Senior Tip: Настройте GOPRIVATE
Обязательно укажите тулчейну, какие домены нельзя искать в публичном прокси:
go env -w GOPRIVATE=github.com/my-secret-org/*,gitlab.company.local/*
Это спасет кучу нервов и предотвратит утечку имен ваших приватных модулей на публичные сервера.
#golang #gomod #architecture #devops #bestpractices
📲 Мы в MAX
👉 @golang_lib👣 Хотите вкатиться в разработку с нуля? Рассмотрите Golang, изучите одну из важных тем языка
📚 На открытом уроке разберём, как устроена типизация в Go, как работают указатели и где хранятся данные — стек, куча или статическая память. Покажем на примерах, как язык управляет памятью и почему это влияет на производительность.
Урок проходит в преддверии старта курса «Go-разработчик. Базовый уровень». Если вы хотите разобраться в базовых механизмах языка и писать код без скрытых ошибок — подключайтесь
🤓Встречаемся 21 мая в 20:00 МСК. Регистрация открыта: https://vk.cc/cXMZhQ
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
🗑 Сборщик мусора в Go: Скрытый налог на ваш CPU
Если вы спросите джуна, как работает память в Go, он ответит: "Ну, там есть GC, он сам всё чистит".
Сеньор же знает: GC - это не волшебная фея, это строгий налоговый инспектор. Он может не забирать ваши деньги (память), но он заберет ваше время (CPU).
Давайте развеем главные мифы и заглянем под капот сборщика мусора в Go.
Миф 1: GC вызывает долгие паузы (Stop-The-World)
Выходцы из старой Java (или те, кто писал на Go до версии 1.5) до сих пор пугают детей долгими паузами, когда приложение буквально замирает на секунды.
В современном Go это не так. Наш GC - это Concurrent Mark and Sweep (Конкурентная пометка и очистка). Фаза Stop-The-World (когда тормозится вообще всё) всё ещё есть, но она занимает доли миллисекунды.
Как он работает (Трехцветный алгоритм):
Представьте, что GC - это маляр, который красит ваши объекты:
1. Белые - объекты-кандидаты на удаление (изначально все такие).
2. Серые - объекты, до которых мы смогли дотянуться из корней (глобальные переменные, стеки горутин), но мы еще не проверили, на что ссылаются они сами.
3. Черные - живые объекты, которые мы проверили полностью.
GC бегает по ссылкам, превращая серые объекты в черные, а новые найденные - в серые. Когда серых не остается, все оставшиеся белые объекты просто стираются из памяти.
И главное - он делает это параллельно с работой вашего кода!
В чем подвох? (Mark Assist)
Если паузы такие короткие, почему мы вообще боремся за zero-allocation код?
Потому что чудес не бывает. GC работает в фоне и забирает под себя до 25% CPU (четверть ваших потоков
P).
Но это еще не всё. Если ваше приложение генерирует мусор (белые объекты) быстрее, чем фоновый GC успевает их красить, планировщик включает режим паники - Mark Assist.
Он буквально берет вашу горутину, которая обрабатывает важный HTTP-запрос пользователя, и говорит: "Слышь, прежде чем я дам тебе память, иди-ка помоги мне покрасить вон те объекты".
В итоге ваш запрос, который обычно отрабатывает за 5мс, внезапно зависает на 50мс. Вы смотрите в логи и ничего не понимаете.
🔥 Senior Tip: Как этим управлять?
В Go почти нет ручек для тюнинга GC, но есть две самые важные переменные окружения:
1. GOGC (по умолчанию 100). Означает "запускать GC, когда куча выросла на 100% от предыдущего размера". Если у вас куча свободной RAM, ставьте GOGC=500. Мусор будет копиться дольше, GC будет запускаться реже, CPU скажет спасибо.
2. GOMEMLIMIT (появилось в Go 1.19). Это "мягкий" лимит памяти. Позволяет сказать GC: "Не запускайся часто, пока мы не упремся в 2 ГБ, а вот если подошли к лимиту - чисти агрессивно". Это спасение от OOM (Out Of Memory) в Kubernetes.
Мораль: Не бойтесь GC, но уважайте его труд. Используйте sync.Pool, заранее аллоцируйте слайсы (make([]int, 0, capacity)) и не передавайте огромные структуры по значению там, где это не нужно.
А вы уже ставили GOMEMLIMIT на проде или до сих пор ловите случайные рестарты подов по OOM? Делитесь в комментах 👇
#golang #underhood #performance #memory #architecture
📲 Мы в MAX
👉 @golang_lib🧬 Generics: Как перестать писать Java на Go
Мы ждали их 10 лет. И вот, когда они появились, код-ревью превратились в выставку угловых скобок. Я видел разработчиков, которые пытались впихнуть дженерики даже в хендлеры HTTP-запросов.
Коллеги, давайте договоримся на берегу: Generics созданы для работы с типами, а не с поведением. Если вам нужно поведение, у нас уже есть интерфейсы.
Давайте разберем, где дженерики это пушка, а где - технический долг.
❌ Как делать НЕ надо (Бизнес-логика)
Типичная ошибка новичка - пытаться объединить несовместимое через
any или огромные union типы, просто чтобы сэкономить пару строк.
// 🤡 Ужасно: Пытаемся сделать "универсальное" сохранение
func SaveToDB[T User | Order | Invoice](db *sql.DB, entity T) error {
// И тут начинается ад из switch type или рефлексии
}
Почему это плохо? Потому что функция все равно должна знать детали каждой структуры, чтобы написать SQL-запрос. Дженерики тут не дают никакой пользы, только усложняют сигнатуру.
Лечение: Используйте интерфейс Saver с методом Save().
✅ Как надо (Структуры данных и Алгоритмы)
Дженерики сияют там, где логике абсолютно плевать, какие данные внутри. Это коллекции (Set, Tree, Queue) и утилитарные функции (Filter, Map).
// 🔥 Отлично: Функция фильтрации слайса
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// Использование:
// evens := Filter([]int{1, 2, 3, 4}, func(v int) bool { return v%2 == 0 })
Мы написали это один раз, и оно работает с int, string, User и чем угодно. Больше никаких interface{} и кастов с паниками!
☝️ Нюанс для Senior-ов (Под капотом):
Многие думают, что дженерики в Go работают как шаблоны в C++ (создают копию функции для каждого типа) или как в Java (Type Erasure).
Go пошел своим путем: GCShape (Garbage Collection Shape).
Компилятор группирует типы с одинаковым размером памяти и расположением указателей. Например, все указатели (*User, *Order) имеют одинаковый GCShape, и для них сгенерируется *только одна* версия функции под капотом, куда передастся невидимый словарь с метаданными. А вот для int и float64 сгенерируются разные версии.
Итог: бинарник пухнет не так сильно, как в C++, а скорость работы почти как у обычных функций.
А вы перешли на пакет samber/lo (Lodash для Go на дженериках) или по старинке пишете for руками? Признавайтесь в комментах! 👇
#golang #generics #cleancode #bestpractices
📲 Мы в MAX
👉 @golang_libRepost from AvitoTech
Коллеги, принесли 🪑🪑🪑 и 🍿🍿🍿
Потому что Райан Гослинг не каждый день про техдолг продаёт!
«Нам нужно заняться техдолгом» — сказал тимлид.
«А можно как-то быстрее?» — ответил бизнес.
И вот вы уже живёте в мире, где:
🔥 система «почти не падает» (и это считается успехом);
🔥 один модуль лучше не трогать вообще;
🔥 хотфиксы делаются «на вечер»;
🔥 а разработчику теперь требуется психолог.
Как же так получается и почему «оно же работает» ≠ стабильность. И да, объяснили, откуда берутся эти магические «две недели вместо трёх месяцев». Приятного просмотра!
📺 YouTube
🔵 ВК Видео
#tl #backend #frontend #go
