en
Feedback
Библиотека Go (Golang) разработчика

Библиотека Go (Golang) разработчика

Open in Telegram

Полезные материалы по всему, что может быть полезно Golang разработчику. По всем вопросам @evgenycarter

Show more
2 691
Subscribers
-124 hours
No data7 days
-1030 days
Attracting Subscribers
July '26
July '26
+15
in 0 channels
June '26
+23
in 0 channels
Get PRO
May '26
+27
in 0 channels
Get PRO
April '26
+47
in 0 channels
Get PRO
March '26
+34
in 0 channels
Get PRO
February '26
+46
in 0 channels
Get PRO
January '26
+40
in 0 channels
Get PRO
December '25
+52
in 0 channels
Get PRO
November '25
+70
in 32 channels
Get PRO
October '25
+66
in 0 channels
Get PRO
September '25
+65
in 36 channels
Get PRO
August '25
+68
in 0 channels
Get PRO
July '25
+70
in 27 channels
Get PRO
June '25
+77
in 20 channels
Get PRO
May '25
+98
in 44 channels
Get PRO
April '25
+100
in 37 channels
Get PRO
March '25
+100
in 39 channels
Get PRO
February '25
+88
in 32 channels
Get PRO
January '25
+89
in 34 channels
Get PRO
December '24
+83
in 35 channels
Get PRO
November '24
+79
in 33 channels
Get PRO
October '24
+94
in 30 channels
Get PRO
September '24
+120
in 29 channels
Get PRO
August '24
+84
in 18 channels
Get PRO
July '24
+70
in 0 channels
Get PRO
June '24
+81
in 24 channels
Get PRO
May '24
+82
in 19 channels
Get PRO
April '24
+71
in 0 channels
Get PRO
March '24
+87
in 21 channels
Get PRO
February '24
+80
in 18 channels
Get PRO
January '24
+141
in 24 channels
Get PRO
December '23
+121
in 24 channels
Get PRO
November '23
+97
in 17 channels
Get PRO
October '23
+116
in 18 channels
Get PRO
September '23
+121
in 0 channels
Get PRO
August '23
+81
in 0 channels
Get PRO
July '23
+116
in 0 channels
Get PRO
June '23
+100
in 0 channels
Get PRO
May '23
+123
in 0 channels
Get PRO
April '23
+75
in 0 channels
Get PRO
March '23
+11
in 0 channels
Get PRO
February '23
+18
in 0 channels
Get PRO
January '23
+32
in 0 channels
Get PRO
December '22
+36
in 0 channels
Get PRO
November '22
+25
in 0 channels
Get PRO
October '22
+50
in 0 channels
Get PRO
September '22
+101
in 0 channels
Get PRO
August '22
+101
in 0 channels
Get PRO
July '22
+137
in 0 channels
Get PRO
June '22
+2 093
in 0 channels
Date
Subscriber Growth
Mentions
Channels
14 July0
13 July0
12 July+1
11 July+2
10 July+1
09 July0
08 July+2
07 July+2
06 July+1
05 July+1
04 July0
03 July+1
02 July+2
01 July+2
Channel Posts
Разработчик приходит в Go из Java, Python или C# — и часто приносит с собой лишние слои, интерфейсы ради интерфейсов и сложну
Разработчик приходит в Go из Java, Python или C# — и часто приносит с собой лишние слои, интерфейсы ради интерфейсов и сложную архитектуру там, где язык требует простоты. 🗓 20 июля в 20:00 МСК приглашаем вас на открытый урок, где мы разберём, как перестроить мышление под философию Go и писать код, который проще читать, сопровождать и защищать на проверке. ❗️На занятии поговорим об интерфейсах, ссылочных типах, строках, указателях, областях видимости, слайсах и мапах. 🔴 Открытый урок проходит в преддверии старта курса «Go-разработчик. Продвинутый уровень». ➡️ Зарегистрируйтесь, чтобы увидеть практический подход к Go без лишних абстракций: https://vk.cc/cZxhMh Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru

2
⚙️ 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
293
3
🕳 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
438
4
Куда уходит память? Разбираемся с 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
595
5
👣 Многие разработчики приходят в Go с багажом паттернов из Java и C#. В результате простой и понятный код постепенно обраста
👣 Многие разработчики приходят в Go с багажом паттернов из Java и C#. В результате простой и понятный код постепенно обрастает слоями абстракций, лишними интерфейсами и десятками DTO, которые усложняют поддержку проекта. 🗓 8 июля в 20:00 МСК приглашаем вас на открытый урок в преддверии старта курса «Go-разработчик. Продвинутый уровень». На занятии разберём, почему привычные подходы из других языков не всегда работают в Go, как выстроить архитектуру через Handler → Service → Repository без циклических зависимостей, где действительно нужны интерфейсы и как избежать избыточных абстракций. ❗️Вы узнаете, какие ошибки чаще всего встречаются на проверках кода, научитесь проектировать приложения с понятным разделением ответственности и поймёте, как писать код, который останется читаемым и через полгода. ➡️ Регистрируйтесь и познакомьтесь с подходом, который помогает писать на Go проще, надёжнее и профессиональнее: https://vk.cc/cZcTF2 Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
570
6
🔀 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
547
7
🚀 Подборка полезных 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 Питер Новости: Санкт-Петербург / СПБ / ДТП
191
8
🔎 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
487
9
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
698
10
🔌 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
763
11
📜 Паттерн 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
869
12
🔍Тестовое собеседование с Go Senior из Uzum в этот четверг 11 июня(в четверг!) в 19:00 по мск приходи онлайн на открытое соб
🔍Тестовое собеседование с Go Senior из Uzum в этот четверг 11 июня(в четверг!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle Go-разработчика. Как это будет: 📂 Маруф Караев, Senior из Uzum, ex-Яндекс, ex-EPAM будет задавать реальные вопросы и задачи разработчику-добровольцу 📂 Маруф будет комментировать каждый ответ респондента, чтобы дать понять, чего от вас ожидает собеседующий на интервью 📂 В конце можно будет задать любой вопрос Маруфу Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Go-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы. Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_go_bot Реклама. О рекламодателе.
667
13
🔄 Идемпотентность: Как не списать деньги дважды при ретраях Худшее, что может сделать ваш микросервис, это упасть с пятисоткой. Нет, вру. Худшее, это тихо выполнить операцию дважды. Все мы знаем, что сеть ненадежна. Поэтому мы оборачиваем 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
692
14
⚙️ Лучшие практики в Go: кейсы, которые особенно ждём С 1 июня у нас стартует новый сезон Podlodka Go Crew — «Лучшие практики
⚙️ Лучшие практики в Go: кейсы, которые особенно ждём С 1 июня у нас стартует новый сезон Podlodka Go Crew — «Лучшие практики в Go». Он пройдёт при поддержке 2ГИС. Будем говорить о том, как писать код, который нормально живёт в продакшене при росте нагрузки и команды. Вот какие сессии в этом сезоне мы ждём с особенным вниманием: 🚀 «Практика Go оптимизаций: растем вместе с нагрузкой», Алексей Акулович — путь сервиса от прототипа до миллионов RPS, оптимизация CPU, grpc, protobuf и даже собственный GC поверх гошного. 🏗 «Эволюция структуры Go-проекта: как 30 человек пушат в один репозиторий», Кирилл Возжеников — про рост продуктового монорепозитория, построение системы и практики, которые помогают не утонуть в хаосе. 🧩 «Как и зачем писать свой CDC на Go», Юра Саргсян — о ситуации, где стандартных решений уже недостаточно. Postgres, Kafka, гарантии доставки и и подводные камни логической репликации. 🔥 А в конце сезона проведём битву кейсов «50 оттенков межсервисного взаимодействия» — вместе разберём архитектурные задачи с метриками, схемами и ограничением на уточняющие вопросы. 👨‍💻 Будет много живых инженерных кейсов и обсуждений без абстракций. Приходите — будем разбирать реальные задачи и подходы, которые помогают строить надёжные системы. 🎟 Билеты здесь: https://podlodka.io/gocrew
763
15
📦 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
710
16
👣 Хотите вкатиться в разработку с нуля? Рассмотрите Golang, изучите одну из важных тем языка 📚 На открытом уроке разберём,
👣 Хотите вкатиться в разработку с нуля? Рассмотрите Golang, изучите одну из важных тем языка 📚 На открытом уроке разберём, как устроена типизация в Go, как работают указатели и где хранятся данные — стек, куча или статическая память. Покажем на примерах, как язык управляет памятью и почему это влияет на производительность. Урок проходит в преддверии старта курса «Go-разработчик. Базовый уровень». Если вы хотите разобраться в базовых механизмах языка и писать код без скрытых ошибок — подключайтесь 🤓Встречаемся 21 мая в 20:00 МСК. Регистрация открыта: https://vk.cc/cXMZhQ Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
827
17
🗑 Сборщик мусора в 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
736
18
🧬 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_lib
693
19
Реклама. Рекламодатель ООО «Авито Тех». erid: 2VtzquWUbod
642
20
Коллеги, принесли 🪑🪑🪑 и 🍿🍿🍿 Потому что Райан Гослинг не каждый день про техдолг продаёт! «Нам нужно заняться техдолгом» — сказал тимлид. «А можно как-то быстрее?» — ответил бизнес. И вот вы уже живёте в мире, где: 🔥 система «почти не падает» (и это считается успехом); 🔥 один модуль лучше не трогать вообще; 🔥 хотфиксы делаются «на вечер»; 🔥 а разработчику теперь требуется психолог. Как же так получается и почему «оно же работает» ≠ стабильность. И да, объяснили, откуда берутся эти магические «две недели вместо трёх месяцев». Приятного просмотра! 📺 YouTube 🔵 ВК Видео #tl #backend #frontend #go
612