Библиотека Go (Golang) разработчика
СтатистикаПолезные материалы по всему, что может быть полезно Golang разработчику. По всем вопросам @evgenycarter
- Последний пост
- 10 авг.
- Последнее чтение
- 12:56
- Постов за неделю
- 1
- Всего постов
- 55
- Тип
- открытый
- Язык
- русский
- Категория
- Технологии
- В каталоге с
- 12 авг.
- 1/24сутки в ленте
- 278
- 1/48двое суток
- 318
- 1/72трое суток
- 343
Оценка по просмотрам недавних постов: пост набирает почти всё за первые сутки.
Посты
♻️ sync.Pool: Как перестать кормить Garbage Collector'а Представьте, что вы работаете в кофейне. Приходит клиент, вы покупаете новую керамическую кружку, наливаете кофе, отдаете клиенту. Он выпивает кофе и... выбрасывает кружку в мусорку. А вы идете покупать новую. Звучит как бред? Но именно так ведет себя ваш код, когда вы создаете буферы make([]byte, 4096) внутри HTTP-хендлера, который обрабатывает 10 000 запросов в секунду. Вы непрерывно выделяете память, а Garbage Collector (тот самый уборщик из прошлых постов) сходит с ума, пытаясь всё это собрать. Сервис тормозит, CPU кипит. Решение проблемы sync.Pool. Это полка с чистыми кружками. Как это работает: Вместо того чтобы создавать объект с нуля, вы просите его у пула (Get). Попользовались - помыли - вернули в пул (Put). ✅ Как это выглядит в коде: var bufPool = sync.Pool{ // Эта функция вызовется ТОЛЬКО если пул пуст // и нам действительно нужно создать новый объект New: func() any { // Выделяем память один раз! return new(bytes.Buffer) }, } func HandleRequest(w http.ResponseWriter, r *http.Request) { // 1. Берем буфер из пула (приводим тип, так как Pool возвращает any) buf := bufPool.Get().(*bytes.Buffer) // 2. ОБЯЗАТЕЛЬНО: Возвращаем буфер в пул при выходе из функции defer bufPool.Put(buf) // 3. КРИТИЧЕСКИ ВАЖНО: Очищаем буфер перед использованием! buf.Reset() // ... используем buf для json.Marshal или io.Copy ... buf.WriteString("hello, world") } 🔥 Нюансы для Senior-ов (Ловушки sync.Pool): 1. Грязные кружки (Утечка данных). Если вы забыли сделать buf.Reset() перед тем как положить буфер обратно (или сразу после того, как достали), следующий гость получит кружку с остатками чужого кофе. В мире микросервисов это значит, что ответ одному пользователю может случайно содержать кусок JSON-а с приватными данными предыдущего пользователя. Это жесточайшая уязвимость. 2. Это не кэш для бизнес-данных! Многие думают: "О, круто, положу-ка я туда настройки из БД, чтобы не ходить за ними каждый раз". Нет. Garbage Collector в Go имеет полное право (и делает это) полностью очистить весь sync.Pool во время сборки мусора. Пул может опустеть в любую миллисекунду. Он предназначен только для переиспользования пустой памяти, а не для хранения состояния. 3. Под капотом: Никаких блокировок (почти). Зачем использовать sync.Pool, а не написать свой кэш на каналах или мьютексах? Потому что sync.Pool гениально оптимизирован под планировщик Go (GMP). Он хранит локальный пул для каждого логического ядра (P). Когда горутина просит объект, она берет его из пула своего ядра вообще без блокировки (lock-free). Мьютексы включаются только тогда, когда локальный пул пуст и нужно "украсть" объект у соседнего ядра. Используйте sync.Pool для []byte, bytes.Buffer, сложных структур парсинга - и ваши графики CPU станут плоскими, как кардиограмма после дедлайна. Кто уже внедрял sync.Pool и ловил баги с нестертыми данными? Признавайтесь 👇 #golang #performance #memory #underhood #bestpractices 📲 Мы в 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 Типичный Сисадмин https://max.ru/channel_win_sysadmin Системный Администратор Windows https://max.ru/channel_linux_admin Linux: Системный администратор https://max.ru/channel_linuxmod Linux https://max.ru/channel_i_linux Системный администратор https://max.ru/channel_devopslib DevOps, SRE, Sysadmin https://max.ru/channel_devops_star DevOps Star (Звезда Девопса) Excel лайфхак 📌 https://t.me/Excel_lifehack Excel лайфхак Английский с нуля 🇬🇧 https://max.ru/UchuEnglish 1C разработка 📌 https://max.ru/odin1c_rus Cтатьи, курсы, советы, шаблоны кода 1С https://max.ru/channel_DevLab1C 1С:Предприятие 8 Программирование C++📌 https://max.ru/cpp_lib Библиотека C/C++ разработчика https://max.ru/channel_cpp_geek C++ geek Программирование Go📌 https://max.ru/golang_lib Библиотека Go (Golang) разработчика Программирование React📌 https://max.ru/react_lib React Программирование Rust📌 https://max.ru/channel_rust_lib Программирование Python 📌 https://max.ru/python_of Python академия. https://max.ru/BookPython Библиотека Python разработчика Java разработка 📌 https://max.ru/bookjava Библиотека Java разработчика https://max.ru/channel_java_geek Java Geek 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 Обучающие видео, книги по Физике и Математике Вакансии в IT📌 https://max.ru/progjob https://max.ru/channel_rabotait Мир технологий 📌 https://max.ru/mir_teh Канал для любознательных Городские📌 https://max.ru/piterspb_78 Свежие новости Санкт-Петербурга https://max.ru/mockva_life Свежие новости Москвы https://max.ru/piterspb Питер Новости: Санкт-Петербург / СПБ / ДТП https://max.ru/channel_krasnodar_novosty Краснодар Новости https://max.ru/channel_novosibirsk_novosti Новосибирск https://max.ru/channel_samara_novosti Новости Самары https://max.ru/channel_ekaterinburg_novosti Новости Екатеринбурга https://max.ru/channel_kazan_novosti Новости Казани https://max.ru/channel_omsk_novosti Новости Омска https://max.ru/channel_moskva_24 Москва 24
🔴 Тестовое собеседование с Go Senior с опытом работы в Яндексе, EPAM и Uzum в этот четверг 6 августа(в четверг!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle Go-разработчика. Как это будет: 📂 Маруф Караев, Senior в европйской компании, ex-Uzum, ex-Яндекс, ex-EPAM будет задавать реальные вопросы и задачи разработчику-добровольцу 📂 Маруф будет комментировать каждый ответ респондента, чтобы дать понять, чего от вас ожидает собеседующий на интервью 📂 В конце можно будет задать любой вопрос Маруфу Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Go-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы. Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_go_bot Реклама. О рекламодателе.
🌊 io.Reader и io.Writer: Лего для взрослых (и спасение от OOM) Знакомый сценарий: микросервис скачивает отчет из внешней системы. Когда вы его писали, отчет весил 5 МБ. Через полгода бизнес вырос, отчет весит 5 ГБ. Ваш под в Kubernetes ловит OOM (Out Of Memory) и бесславно умирает. Что делает в такой ситуации джуниор? Пытается увеличить лимиты памяти в Helm-чартах. Почему так вышло? Потому что код выглядит вот так: ❌ Плохо (Смерть памяти): resp, _ := http.Get(url) // Вычитываем ВСЕ 5 Гигабайт в оперативную память data, _ := io.ReadAll(resp.Body) os.WriteFile("report.csv", data, 0644) В Go есть два интерфейса, на которых держится вообще вся стандартная библиотека. Это io.Reader и io.Writer. В них всего по одному методу (Read и Write), но они превращают ваш код в систему сообщающихся сосудов. Суть этих интерфейсов - стриминг (потоковая передача данных). Нам не нужно держать в памяти весь файл, чтобы переложить его из сети на диск или отправить другому сервису. ✅ Хорошо (Стриминг через io.Copy): resp, _ := http.Get(url) defer resp.Body.Close() out, _ := os.Create("report.csv") defer out.Close() // Магия потока! io.Copy(out, resp.Body) Знаете, сколько оперативной памяти съест io.Copy при скачивании файла на 5 ГБ? Ровно 32 килобайта. Под капотом функция создает маленький буфер, читает в него кусок данных из сети и тут же сбрасывает на диск. И так по кругу, пока поток не иссякнет. 🔥 Нюансы для Senior-ов: Композиция (Пайплайны) Настоящая мощь io раскрывается, когда вы начинаете собирать эти интерфейсы в цепочки (паттерн Декоратор). Они вставляются друг в друга, как матрешки. Представьте задачу: нужно скачать файл, на лету распаковать его из GZIP и посчитать SHA256-хэш, не сохраняя ничего на диск. resp, _ := http.Get("http://example.com/data.gz") defer resp.Body.Close() // 1. Оборачиваем сетевой поток в GZIP-распаковщик (он тоже io.Reader!) gzReader, _ := gzip.NewReader(resp.Body) defer gzReader.Close() // 2. Создаем "писателя", который считает хэш hash := sha256.New() // 3. io.TeeReader читает из gzReader и параллельно дублирует данные в hash tee := io.TeeReader(gzReader, hash) // 4. Сливаем данные в "никуда" (io.Discard), // чтобы заставить насос качать данные по нашей трубе io.Copy(io.Discard, tee) fmt.Printf("Хэш файла: %x\n", hash.Sum(nil)) Мы только что обработали гигабайты сжатых данных за долю секунды, используя копейки RAM, ни разу не коснувшись жесткого диска. Интерфейсы io - этоUnix-философия, зашитая прямо в язык. Освойте их, и вы перестанете бояться огромных объемов данных. #golang #architecture #performance #io #bestpractices 📲 Мы в MAX 👉 @golang_lib
Как спасти сборщик мусора от перегрева: используем sync.Pool 🛟 В продолжение темы про аллокации в куче. Если ваш высоконагруженный бэкенд создает тысячи временных объектов в секунду (например, буферы для сборки ответов на HTTP-запросы или парсинга JSON), сборщик мусора (GC) начинает задыхаться, сжигая драгоценное процессорное время. Чтобы не выделять память каждый раз заново, в Go есть встроенный и мощный инструмент - sync.Pool. Что это такое? Это потокобезопасный механизм для хранения и переиспользования временных объектов. Как это работает: • Метод Get() достает объект из пула. Если пул пуст, автоматически вызывается функция New, которая создает новый экземпляр. • Метод Put() возвращает объект обратно в пул после того, как он стал не нужен. Где это реально полезно? Идеальный кандидат для пула - bytes.Buffer, массивы байт для чтения из сети или тяжелые структуры данных. Популярные пакеты вроде fmt, encoding/json или сверхбыстрый логгер zap от Uber активно используют sync.Pool под капотом именно для снижения нагрузки на GC. Важные нюансы (на которых часто обжигаются): • Это не кэш. Сборщик мусора имеет полное право очистить sync.Pool в любой момент (обычно во время очередного цикла сборки), удалив объекты, которые сейчас не используются. Не пытайтесь хранить там постоянные соединения с БД или пользовательские сессии. • Всегда сбрасывайте состояние. Перед тем как вернуть объект через Put(), его нужно очистить (например, вызвать Reset()). Иначе следующая горутина, вызвавшая Get(), получит объект с чужими «грязными» данными, что приведет к плавающим и трудноотловимым багам. Пример правильного использования: var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) // Вызывается только если пул пуст }, } func process() { // Берем буфер из пула и кастуем к нужному типу buf := bufPool.Get().(*bytes.Buffer) // Гарантируем очистку и возврат буфера defer func() { buf.Reset() // Очищаем старые данные! bufPool.Put(buf) }() // ... работаем с buf ... } #golang #backend #performance #память 📲 Мы в MAX 👉 @golang_lib
Базовый стек межсервисного взаимодействия и наблюдаемости: HTTP(S), gRPC, Protobuf и OpenTelemetry Современная микросервисная архитектура требует стандартизированных подходов к транспорту, сериализации данных и мониторингу. Понимание данного набора технологий необходимо для проектирования, эксплуатации и отладки распределенных систем. HTTP(S) Фундаментальный протокол взаимодействия. Базовые знания должны включать не только структуру запроса и ответа (заголовки, методы, коды состояния), но и механизмы работы на уровне сетевого стека: • Особенности установки защищенного соединения (TLS-хендшейк, управление сертификатами). • Управление постоянными соединениями (Keep-Alive) и пулинг соединений (Connection Pooling). • Архитектурные различия между версиями HTTP/1.1, HTTP/2 (мультиплексирование, бинарный фрейминг) и HTTP/3 (QUIC, устранение проблемы head-of-line blocking). gRPC Высокопроизводительный RPC-фреймворк, использующий HTTP/2 в качестве транспортного уровня. • Оптимизирован для межсервисного взаимодействия (backend-to-backend) за счет снижения накладных расходов. • Поддерживает классические унарные вызовы, а также серверный, клиентский и двунаправленный стриминг. • Требует понимания специфики балансировки нагрузки (L7) и обработки таймаутов/разрывов соединений на уровне прокси-серверов. Protocol Buffers (Protobuf) Бинарный формат сериализации структурированных данных, являющийся стандартом для gRPC. • Обеспечивает строгую типизацию данных и генерацию кода для различных языков программирования. • Гарантирует обратную и прямую совместимость API за счет жесткой нумерации полей (отсутствие необходимости в версионировании эндпоинтов по аналогии с REST). • Обеспечивает минимальный размер полезной нагрузки и высокую скорость сериализации/десериализации по сравнению с JSON или XML. OpenTelemetry (OTel) Единый стандарт (CNCF) для сбора и экспорта метрик, логов и распределенных трассировок. • Позволяет абстрагироваться от конкретных вендоров систем мониторинга (Jaeger, Prometheus, ClickHouse) за счет использования стандартизированного протокола OTLP (OpenTelemetry Protocol). • Обеспечивает сквозную трассировку запроса при прохождении через инфраструктуру и микросервисы. • Требует понимания концепции Context Propagation - проброса идентификаторов (trace_id, span_id) через метаданные запросов (например, с использованием стандарта W3C Trace Context в HTTP-заголовках или gRPC-метаданных). Интеграция компонентов Данные технологии работают в неразрывной связке. Структуры данных описываются в Protobuf, компилируются и передаются между микросервисами посредством gRPC поверх мультиплексированных соединений HTTP/2. Весь жизненный цикл запроса инструментируется библиотеками OpenTelemetry, что позволяет локализовать задержки на уровне сети, сериализации или бизнес-логики. Понимание работы каждого уровня обязательно для эффективного траблшутинга и профилирования систем под высокой нагрузкой. 📲 Мы в MAX 👉 @golang_lib
👣 Почему всё больше инфраструктурных проектов, облачных сервисов и высоконагруженных систем создают именно на Go? Если вы до сих пор воспринимаете этот язык как нишевый инструмент, возможно, пришло время взглянуть на него по-новому. 🗓 28 июля в 20:00 МСК приглашаем вас на открытый урок в преддверии старта курса «Go-разработчик. Продвинутый уровень». Разберём, почему Go стал стандартом для создания современных сервисов и какие задачи он помогает решать быстрее и надёжнее. ❗️Вы узнаете, где сегодня применяется Go: в микросервисах, облачной инфраструктуре, DevOps, высоконагруженных системах и информационной безопасности. Обсудим, какие преимущества язык даёт разработчикам, какие навыки востребованы работодателями и почему интерес к Go продолжает расти. ➡️ Если вы хотите уверенно развиваться в современной разработке, понять перспективы языка и определить, где он принесёт максимальную пользу именно вам, зарегистрируйтесь на открытый урок: https://vk.cc/cZMQZ0 Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
🚀 PGO: Как получить +10% к скорости, не написав ни строчки кода Все мы любим оптимизировать. Переписываем мапы, пулим объекты в sync.Pool, боремся с аллокациями. Но что, если я скажу, что в новых версиях Go (начиная с 1.21) можно ускорить приложение на 5-10%, просто подкинув компилятору один файлик? Profile-Guided Optimization (PGO). В чем проблема обычного компилятора? При стандартной сборке компилятор опирается на эвристики. Он смотрит на функцию и гадает: "Наверное, эту функцию вызывают часто, давай-ка я её заинлайню (inline), чтобы сэкономить на вызове". Но компилятор не знает, как ваш код ведет себя в реальном продакшене. Что меняет PGO? PGO ломает этот слепой подход. Вы берете профиль нагрузки (CPU profile) с реально работающего продакшена и отдаете его компилятору при сборке следующего релиза. Компилятор смотрит в профиль: "Ага, вот эта функция processOrder жрет 30% CPU, инлайним её агрессивно! А эта handleError вызывается раз в год - убираем её с горячего пути, чтобы не засорять кэш процессора". Как это сделать (3 простых шага): 1. Собираем профиль с прода. Идем на боевой (или нагрузочный) сервер, где подключен net/http/pprof, и стягиваем 30-секундный профиль: curl -o default.pgo http://prod-server:8080/debug/pprof/profile?seconds=30 2. Кладем файл в корень проекта. Просто кидаете файл default.pgo в главную директорию вашего модуля (там же, где лежит go.mod). 3. Собираем как обычно. go build -o myapp Всё. Начиная с Go 1.21.2, флаг -pgo=auto включен по умолчанию. Компилятор сам найдет файл default.pgo и оптимизирует бинарник. ☝️ Нюансы для Senior-ов: • А что если исходный код изменился? PGO в Go спроектирован устойчивым к изменениям (robust). Если вы собрали профиль, а потом немного порефакторили код, компилятор не сойдет с ума. Он применит оптимизации там, где функции совпали, и безопасно проигнорирует несовпадения. • Где брать профиль для CI/CD? Настройте автоматический сбор профиля с продакшена (например, раз в неделю) и коммитьте его прямо в репозиторий. Да, бинарный файл в гите - звучит как ересь, но для PGO это официальная рекомендация от команды Go. #golang #performance #pgo #optimization 📲 Мы в MAX 👉 @golang_lib
🚀 Базовые паттерны проектирования в Go: Пишем чистый код Go не является классическим объектно-ориентированным языком. Здесь нет классов и наследования в привычном понимании, поэтому многие "книжные" паттерны (GoF) реализуются иначе. В Go делается упор на композицию, неявные интерфейсы и функции высшего порядка. Давайте разберем основные паттерны, которые чаще всего встречаются в продакшен-коде на Go. 🛠 Порождающие паттерны (Creational) Эти паттерны решают задачи безопасного и удобного создания объектов. • Factory (Фабрика): В Go фабрики обычно представляют собой функции, начинающиеся с New... (например, NewService()). Чаще всего они возвращают интерфейс, а не конкретную структуру. Это скрывает внутреннюю реализацию и сильно упрощает написание моков для тестов. • Singleton (Одиночка): Гарантирует, что у объекта есть только один экземпляр. В Go он канонично и безопасно реализуется с помощью sync.Once. Это защищает от состояния гонки (race condition) при инициализации в конкурентной среде. • Builder (Строитель): Используется для создания сложных объектов с множеством параметров. В современном Go классический Builder часто заменяют более элегантным паттерном Functional Options (передача функций-конфигураторов прямо в конструктор). 🏗 Структурные паттерны (Structural) Отвечают за построение удобных и гибких связей между компонентами. • Decorator (Декоратор): Позволяет динамически наслаивать новое поведение. В мире Go это абсолютная база для написания middleware в HTTP-серверах (например, логирование, CORS, авторизация), где одна функция-обработчик оборачивается в другую. • Adapter (Адаптер): Позволяет объектам с несовместимыми интерфейсами работать вместе. Реализуется через создание структуры, которая удовлетворяет нужному интерфейсу, а под капотом делегирует вызовы другому объекту. ⚙️ Поведенческие паттерны (Behavioral) Управляют тем, как объекты взаимодействуют друг с другом. • Strategy (Стратегия): Позволяет менять алгоритм работы на лету. В Go это делается максимально просто: бизнес-логика ожидает любой тип, реализующий определенный интерфейс. Вы просто подменяете реализацию (например, стратегию кэширования: Redis или In-Memory). • Observer (Наблюдатель): Механизм подписки на события. В Go для этого часто не нужны сложные структуры - паттерн отлично ложится на встроенные каналы (channels) и горутины. ⚡️ Конкурентные паттерны (Concurrency) Поскольку параллелизм - главная фишка Go, здесь есть свои специфичные паттерны: • Worker Pool (Пул воркеров): Ограничивает количество одновременно работающих горутин, чтобы не исчерпать ресурсы системы (например, лимит соединений с БД). Задачи отправляются в один канал, а горутины-воркеры их оттуда разбирают. • Fan-out / Fan-in: Распределение тяжелой задачи на множество параллельных горутин (Fan-out) и последующий сбор результатов их независимой работы в единый результирующий канал (Fan-in). 💡 Главный совет: Не пытайтесь натянуть классические ООП-паттерны из Java или C# на Go "один в один". Используйте сильные стороны языка. Idiomatic Go (идиоматичный код) всегда строится на простоте. #golang #go #разработка #паттерны #программирование #godev #архитектура 📲 Мы в MAX 👉 @golang_lib
Разработчик приходит в 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
🔎 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_lib
Connection 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 Реклама. О рекламодателе.