Библиотека Go-разработчика | Golang
Все самое полезное для Go-разработчика в одном канале. Учиться у нас: clc.to/qaSdww По рекламе: @proglib_adv Для обратной связи: @proglibrary_feeedback_bot РКН: https://gosuslugi.ru/snet/67a4a8c24689c2151c752af0 #WXSSA
Показати більше📈 Аналітичний огляд Telegram-каналу Библиотека Go-разработчика | Golang
Канал Библиотека Go-разработчика | Golang (@goproglib) у мовному сегменті Російська є активним учасником. На даний момент спільнота об'єднує 23 949 підписників, посідаючи 5 620 місце в категорії Технології та додатки та 27 758 місце у регіоні Росія.
📊 Показники аудиторії та динаміка
З моменту свого створення невідомо, проект продемонстрував стрімке зростання, зібравши аудиторію у 23 949 підписників.
За останніми даними від 28 червня, 2026, канал демонструє стабільну активність. Хоча за останні 30 днів спостерігається зміна кількості учасників на -86, а за останні 24 години на -4, загальне охоплення залишається високим.
- Статус верифікації: Не верифікований
- Рівень залученості (ER): Середній показник залученості аудиторії становить 11.88%. Протягом перших 24 годин після публікації контент зазвичай збирає 7.58% реакцій від загальної кількості підписників.
- Охоплення публікацій: В середньому кожен допис отримує 2 844 переглядів. Протягом першої доби публікація в середньому набирає 1 816 переглядів.
- Реакції та взаємодія: Аудиторія активно підтримує контент: середня кількість реакцій на один пост – 10.
- Тематичні інтереси: Контент зосереджений навколо ключових тем, таких як навигация, лучшее_из_библиотеки_2025, git, string, golive.
📝 Опис та контентна політика
Автор описує ресурс як майданчик для висловлення суб'єктивної думки:
“Все самое полезное для Go-разработчика в одном канале.
Учиться у нас: clc.to/qaSdww
По рекламе: @proglib_adv
Для обратной связи: @proglibrary_feeedback_bot
РКН: https://gosuslugi.ru/snet/67a4a8c24689c2151c752af0
#WXSSA”
Завдяки високій частоті оновлень (останні дані отримано 29 червня, 2026), канал підтримує актуальність та високий рівень охоплення публікацій. Аналітика показує, що аудиторія активно взаємодіє з контентом, що робить його важливою точкою впливу в категорії Технології та додатки.
uuid.NewV7() теряет случайность в браузере
— gcli v3.8.0
— Добавить slog.TestHandler в стандартную библиотеку Go
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoLivefunc FuzzParse(f *testing.F) {
f.Add("platform=5000") // сиды это ваши готовые кейсы
f.Fuzz(func(t *testing.T, rule string) {
team, limit, err := ParseBudgetRule(rule)
if err != nil {
return // ошибка на кривом входе это норма
}
// раз ошибки нет, результат обязан быть валидным
if limit <= 0 || team == "" {
t.Errorf("bad result without error for %q", rule)
}
})
}
Частое заблуждение, что фаззер сыплет случайные байты. Современные фаззеры coverage‑guided. Стартуют с ваших сидов и мутируют их, отслеживая, какие мутации открывают новые ветки. Попал в новый путь, копает оттуда. Поэтому к багам он сходится в разы быстрее слепого рандома. Юнит‑тесты проверяют дороги, которые вы построили, фаззер ищет те, о которых вы не знали.
❓ Что ловит
Класс ошибок на стыке кода и реальности. Паника на входе, которого «не бывает».
parts := strings.SplitN(rule, "=", 2)
limit, _ := strconv.Atoi(parts[1]) // "platform" без = и parts[1] не существует
// panic: index out of range [1] with length 1
Нарушения инвариантов, когда функция вернула мусор без ошибки, что хуже явной паники. Расхождения round‑trip, когда decode(encode(x)) не равно x. Их объединяет одно. Все появляются, когда снаружи приходит то, чего код не ждал. Обычные тесты это скипают, потому что их пишут под ожидаемые сценарии, а вы рассуждаете как автор кода, который знает, как им пользоваться. Фаззеру эта рамка незнакома.
Найденный падающий вход не исчезает. Go сам кладёт минимальный контрпример в testdata/fuzz/ и делает из него регрессию:
go test -fuzz=FuzzParse -fuzztime=30s # ищем баг go test ./... # корпус гоняется всегда, даже без -fuzzОдин раз пойманный баг вернуться уже не может. ➡️ Куда прикладывать Туда, где код разбирает или валидирует внешний вход. Парсеры, декодеры, заголовки, query, конфиги, всё, что ветвится по содержимому. Чем больше условной логики, тем больше путей фаззеру. Не стоит фаззить чистую бизнес‑логику без зависимости от входа и функции, которые ходят в сеть или базу, их фаззер дёрнет тысячи раз. Правило короткое. Зависит поведение от того, что прислал внешний мир, заведите фаззинг. ❓ Почему окупается Барьер почти нулевой. В Go фаззер встроен в тулчейн с версии 1.18, свойство пишется в считаные строки. Цена это пара минут на разработке. Альтернатива это ждать, пока вход за вас подберёт прод. Боевыми данными, под нагрузкой, в три часа ночи, в виде алерта. Прод пофаззит ваш код в любом случае, вопрос лишь в том, узнаете вы о баге от теста или от дежурного. 📍 Навигация: Вакансии • Задачи • Собесы 🐸 Библиотека Go-разработчика #GoDeep
/cmd /internal /handlers /services /repository /models /pkgВыглядит разумно и проходит ревью. Беда в том, что папки создают иллюзию границ, но не держат их. Ничто не мешает хендлеру импортировать репозиторий напрямую. Ничто не мешает модели обрасти бизнес‑логикой. Границы косметические, и код деградирует предсказуемо. Логика переползает туда, где удобнее, обычно в хендлеры, потому что там контекст. Типы репозитория протекают в API, потому что заводить отдельный DTO лень. Пакет
models превращается в общий мешок структур, от которого зависят все, и поменять в нём что‑либо без каскада уже нельзя. Получается слоёная на вид архитектура, ведущая себя как монолит с накладными расходами на HTTP.
В чём контракт слоёв
Контракт про направление зависимостей. Они текут внутрь. Внешний слой (HTTP, gRPC, CLI) знает про слой приложения. Слой приложения знает про домен. Домен не знает ни про что.
На практике это значит вот что. Хендлер принимает запрос, зовёт метод сервиса и сериализует ответ. Никакого SQL и никаких бизнес‑правил в нём нет.
Сервис кодирует, что приложение делает. Он оркестрирует, валидирует, делегирует репозиторию и ничего не знает про HTTP. Репозиторий говорит с базой и возвращает доменные типы или ошибки, но не pgx.Rows.
Доменные типы несут инварианты. Структура Member знает, что делает участника валидным, но не знает, как сериализовать себя в JSON. Когда контракт держится, слои тестируются независимо, реализацию репозитория можно подменить, не трогая хендлер, а gRPC добавить без дублирования логики.
Анти‑паттерн god service
Чаще всего контракт ломает god service. Сервис‑структура, которая всасывает всё:
type MemberService struct {
db *pgxpool.Pool
}
func (s *MemberService) CreateMember(ctx context.Context, req CreateMemberRequest) (*Member, error) {
// валидация, хеш, вставка строки, welcome‑письмо, событие в Kafka
}
Через полгода у MemberService сорок методов, пул базы, почтовый клиент, продюсер Kafka, клиент S3 и конфиг. Любой тест требует поднять все зависимости, даже когда проверяешь одну ветку валидации. Это уже не слой, а ящик для хлама. И дробить его на MemberCreationService и MemberUpdateService бесполезно, получите ящики поменьше.
Лечит инверсия зависимостей
Интерфейсы в Go удовлетворяются неявно, и это недоиспользуют. Вместо *pgxpool.Pool опишите то, что сервису реально нужно:
type MemberRepository interface {
CreateMember(ctx context.Context, params CreateMemberParams) (*Member, error)
GetMemberByID(ctx context.Context, id uuid.UUID) (*Member, error)
}
type EventPublisher interface {
Publish(ctx context.Context, event DomainEvent) error
}
type MemberService struct {
repo MemberRepository
publisher EventPublisher
}
Теперь сервис ничего не знает про PostgreSQL и Kafka. Он зависит от поведения, а не от реализации. Чтобы протестировать метод, достаточно мока под интерфейс, без базы, брокера и сети. Про pgx знает реализация репозитория, про franz-go знает реализация publisher, а сервис не знает ни про то, ни про другое. В этом и есть контракт.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoDeepcontext.WithValue ничего не меняет, а создаёт новую обёртку, поэтому контекст безопасно шарить между горутинами. Учебники забывают сказать главное.
Иммутабельны обёртки, а объект в корне цепочки может переиспользоваться фреймворком через sync.Pool. Тогда горутина, пережившая запрос, держит указатель на память, которая вот‑вот достанется другому запросу.
Хендлер запускает фоновую задачу и сразу возвращает ответ. Линтер ругается на отмену контекста, и со времён Go 1.21 его успокаивают одной строкой:
go s.processAsync(context.WithoutCancel(ctx), id)Для
net/http это правильно. Там контекст это обычная аллокация в куче. Но линтер слеп к архитектуре и не знает, что под context.Context может стоять пулящаяся структура.
❓ Где можно споткнуться
В Fiber и всём на fasthttp c.Context() возвращает *fasthttp.RequestCtx. Пулящийся объект и есть ваш контекст, и после отправки ответа fasthttp сбрасывает его и кладёт обратно в пул. Gin пулит *gin.Context, поэтому передавать его в горутину нельзя (для снимка есть c.Copy()), а c.Request.Context() безопасен. Echo свой контекст пулит, но context.Context он не реализует, так что задеть гонку сложнее.
Дальше воркер просыпается после I/O, зовёт ctx.Value("user_id") и читает память, которую перезаписывают данными другого пользователя. И вот что коварно. WithoutCancel снимает только отмену, а Value() по‑прежнему делегирует пулящемуся родителю. То есть фикс, который позеленил линтер, ровно и удерживает протухший указатель.
➡️ Как правильно
Если задаче ничего не нужно из запроса, постройте свежий корень:
ctx := context.Background()
Если нужны trace‑id или user‑id, вытащите их как простые значения, пока горутина запроса ещё жива, и положите в свежий контекст:
userID := c.Locals("user_id").(string)
traceID := c.Locals("trace_id").(string)
go func(userID, traceID string) {
ctx := context.WithValue(context.Background(), userCtxKey, userID)
ctx = context.WithValue(ctx, traceCtxKey, traceID)
h.svc.ProcessAsync(ctx, userID)
}(userID, traceID)
Вся безопасность держится на том, что значения сняты до возврата из хендлера. Логику стоит вынести в хелпер вроде ctxutil.Detach(ctx). Из штатных лазеек помогают c.Copy() у Gin и c.UserContext() у Fiber.
Линтер видит синтаксис, но не жизненный цикл памяти. Если горутина переживает запрос, не давайте ей контекст запроса ни в какой обёртке. Снимайте значения как простые данные, стройте свежий корень и держите -race в CI.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoDeepgit clone https://github.com/gophish/gophish.git
cd gophish
go build
./gophish
Есть и официальный Docker образ, если не хотите ставить ничего на хост:
docker run --name gophish -p 3333:3333 -p 8080:8080 gophish/gophish
После старта админка поднимается на https://localhost:3333. Логин и пароль не зашиты заранее, а печатаются в лог при первом запуске. Выглядит это примерно так:
level=info msg="Please login with the username admin and the password 4304d5255378177d"Gophish превращает разрозненную ручную работу по фишинговым учениям в управляемый процесс с нормальной статистикой. ➡️ Репозиторий 📍 Навигация: Вакансии • Задачи • Собесы 🐸 Библиотека Go-разработчика #GoToProduction
func foo() {}
Тело есть. Но оно пустое. Функция ничего не делает, ничего не возвращает.
Go — язык строгий. Он не любит неиспользуемые переменные и лишний импорт. Но как он относится к функции, которая существует и при этом ничего не делает?
Подсказка: вспомните, как Go обрабатывает неиспользуемый код на уровне функций, а не переменных.
➡️ Правильный ответ
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#ReadySetGolog/slog готовый хендлер, который пишет логи в testing.TB. Тогда логи приложения и логи теста окажутся в одном выводе и привяжутся к нужному тесту или подтесту.
❓ Зачем это
Сопоставлять логи приложения с тестом, который их породил, приходится постоянно. Кирпичик для этого уже есть. Метод testing.TB.Output() добавили в Go 1.25 для T, B и F, а в Go 1.26 расширили на сам интерфейс TB. Но готового хендлера в slog нет, поэтому каждый проект собирает его заново.
Обычно так:
logger := slog.New(slog.NewTextHandler(t.Output(), nil))
Этот вариант работает, но у него два минуса. Удобства тест ориентированного хендлера приходится дописывать руками, а наивная форма имеет реальную гонку при логировании после завершения теста.
➡️ Что предлагают сделать
В log/slog появится тип TestHandler, реализующий интерфейс Handler, и конструктор к нему. Чтобы не тянуть testing внутрь slog и не словить циклический импорт, вводят узкий интерфейс TestLogger. Ему удовлетворяют *testing.T, *testing.B и *testing.F.
package slog
// TestLogger — подмножество testing.TB, нужное хендлеру.
type TestLogger interface {
Output() io.Writer
}
// NewTestHandler возвращает Handler, который пишет записи в tb.Output().
// Вывод форматируется как у NewTextHandler. opts может быть nil.
func NewTestHandler(tb TestLogger, opts *HandlerOptions) *TestHandler
type TestHandler struct {
tb TestLogger // куда писать
// ...
}
func (h *TestHandler) Enabled(ctx context.Context, level Level) bool { ... }
func (h *TestHandler) Handle(ctx context.Context, record Record) error { ... }
func (h *TestHandler) WithAttrs(attrs []Attr) Handler { ... }
func (h *TestHandler) WithGroup(name string) Handler { ... }
На стороне теста всё сводится к одной строке:
func TestThing(t *testing.T) {
logger := slog.New(slog.NewTestHandler(t, nil))
// Логи лягут под TestThing с отступом от раннера.
// Для прошедших тестов вывод скрыт, пока не передан флаг -v,
// как у t.Log() и t.Logf().
doWork(logger)
}
Пока это обсуждение, а не принятая фича. Следить за судьбой можно в самом issue #80138.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoLiveGoFr берёт обвязку на себя. Это фреймворк, заточенный под микросервисы и деплой в Kubernetes.
❓ Как это выглядит
Минимальное приложение умещается в десяток строк. Сначала ставите модуль:
go get -u gofr.dev/pkg/gofr
Дальше создаёте приложение и вешаете маршрут:
package main
import "gofr.dev/pkg/gofr"
func main() {
app := gofr.New()
app.GET("/greet", func(ctx *gofr.Context) (any, error) {
return "Hello World!", nil
})
app.Run() // слушает на localhost:8000
}
После этого по адресу localhost:8000/greet уже отвечает рабочий сервис. Обработчик возвращает данные и ошибку, а сериализацию ответа по стандартам REST фреймворк делает сам.
➡️ Что внутри
Главная фишка это наблюдаемость из коробки. Логи, трейсы и метрики собираются без ручной настройки, под капотом OpenTelemetry. Конфиги читаются из переменных окружения и файлов, так что код не привязан к окружению. Подключения к базам данных идут через объект ctx, и для каждого источника данных есть health чек.
Фреймворк закрывает типичные потребности микросервиса: поддержка gRPC, HTTP клиент с circuit breaker, Pub/Sub, миграции базы, cron задачи, аутентификационный middleware и место под свой, смена уровня логирования без перезапуска, рендеринг Swagger, абстракция над файловыми системами, веб сокеты.
GoFr подойдёт, если вы строите много однотипных сервисов и устали раз за разом писать одну и ту же инфраструктурную обвязку. Взамен вы принимаете правила фреймворка, потому что он именно опинионированный и навязывает свой способ делать вещи.
Для разовой утилиты или нестандартной архитектуры это может оказаться лишним, а вот для парка микросервисов под Kubernetes экономия времени ощутимая.
➡️ Репозиторий
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoToProductionДля этого курса мы ищем классных спикеров-практиков.➡️ Что требуется от вас? ● BigTech бэкграунд: опыт работы разработчиком, тимлидом или архитектором в крупных технологических компаниях. ● Опыт управляемой разработки с ИИ: вы на практике знаете, как встроить AI-инструменты в личный или командный workflow (от постановки задачи до ревью AI-кода). ● Системный подход: умение превращать хаотичную генерацию кода в предсказуемый инженерный процесс (работа с архитектурой, чек-листами, легаси и покрытием тестами). ➡️ Что мы предлагаем? ● Достойную оплату за подготовку материалов и проведение занятий. ● Мощное продвижение личного бренда через медиаресурсы Proglib (наша аудитория — 1 млн+ айтишников). ● Возможность публиковать свои экспертные материалы и статьи на наших площадках. ● Доступ к сильному и закрытому профессиональному сообществу. Также мы ищем консультанта программы. Если вы практикующий эксперт и готовы помочь нам отвалидировать программу курса, дать рекомендации по актуальности тем и финальным результатам обучения — мы вас очень ждем. ➡️ Как с нами связаться: Telegram: @alinaa_kh E-mail: alina@proglib.io
gcli это Go библиотека для консольных приложений. Она берёт на себя разбор команд и флагов, цвет в терминале, диалоги с пользователем, прогресс бары и генерацию автодополнения для bash и zsh.
🌻 Общие опции для подкоманд
Раньше, если один и тот же флаг нужен нескольким подкомандам, его приходилось объявлять в каждой заново.
В 3.8.0 появилась трёхслойная модель опций. Через Command.SharedOpts вы задаёте общие флаги один раз, и при разборе они подмешиваются в подкоманды.
Слияние идемпотентно, локальная опция перекрывает общую, а проверка Required теперь смотрит на тип значения, а не просто на пустую строку.
Рядом добавили примитив Parser.InheritOptsFrom для слияния наборов опций и аксессор CliOpt.TypeName() для доступа к имени типа. В выводе help унаследованные флаги теперь показываются отдельной группой Inherited Options, так что пользователю видно, откуда взялся флаг.
🌻 Генерация документации из команд
Второе крупное изменение это пакет docgen. Из описания ваших команд можно собрать markdown через CmdMarkdown, AppMarkdown и MarkdownTree, либо man страницы через CmdMan и ManTree.
Встроенная команда GenDoc экспортирует готовые md и man файлы без лишнего кода. Многострочные примеры в man теперь сохраняются как есть, а цветовые теги из примеров вычищаются перед записью.
🌻 Рефакторинг и фиксы
Внутри вынесли AppOptions, и состояние разбора больше не висит на общем синглтоне для каждого приложения. Это убирает странности, когда в одном процессе крутится несколько приложений. Ещё поправили разворачивание анонимных встроенных структур неэкспортируемых типов в FromStruct.
➡️ Релиз
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoLiveСервис Транспорта строит маршруты во всех продуктах 2ГИС, отображает пробки, дорожные события и автобусы на карте. Social сотворяет магию вокруг социального графа. Platform Backend Services превращает бизнес‑требования в платформенные сервисы: от идеи до запуска фич мобильными командами без лишних зависимостей. Web API решает все справочные задачи, управляет стилями карт и обратной связью от пользователей в продуктах. Сервис Рекламы создаёт техническую основу для продуктов рекламного направления. 2ГИС Логистика строит и пересчитывает маршруты с учётом пробок, погодных условий, типов транспорта, параметров груза и сложных логистических цепочек. ГеоПоток помогает бизнесу повышать прозрачность процессов и сокращать издержки.Все вакансии на сайте Другие инженерные инсайты от 2ГИС → в Telegram-канале RnD
recover завершает весь процесс. Острая грань языка, про которую легко забыть.
➡️ Что случилось
Был шлюз вебхуков, который параллельно рассылал события десяткам подписчиков. У одного оказался кривой URL, который в редком случае приводил к панике в HTTP-клиенте вместо ошибки.
Раздача шла в голых горутинах без восстановления:
func fanOut(events []Event, subscribers []Subscriber) {
var wg sync.WaitGroup
for _, sub := range subscribers {
wg.Add(1)
go func(s Subscriber) {
defer wg.Done()
deliver(s, events) // паника здесь убивает весь процесс
}(sub)
}
wg.Wait()
}
В Go паника в любой горутине, которую не перехватили через recover, завершает всю программу. Не важно, сколько других горутин в этот момент делают полезную работу.
Про это легко забыть, потому что в туториалах паника обычно происходит в главной горутине, где последствия очевидны. Здесь же баг одного подписчика оборвал доставку всем.
➡️ Как починили
Изоляция паники стала структурным паттерном, а не заплаткой по случаю. Появилась обёртка safeGo:
func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("recovered panic: %v\n%s", r, debug.Stack())
}
}()
fn()
}()
}
func fanOut(events []Event, subscribers []Subscriber) {
var wg sync.WaitGroup
for _, sub := range subscribers {
wg.Add(1)
safeGo(func() {
defer wg.Done()
deliver(sub, events)
})
}
wg.Wait()
}
Теперь каждая долгоживущая или fan-out горутина проходит через обёртку вроде safeGo, и серверные gRPC-интерсепторы получили то же самое. Восстановление после паники переехало с верхнего HTTP-хендлера на каждую горутину, которую вы заводите сами.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoToProductionuuid.NewV7() теряет случайность в браузере
В трекере Go появился любопытный баг: новый стандартный пакет uuid, который должен приехать в Go 1.27, на таргете js/wasm генерирует «бракованные» UUIDv7 — часть случайных бит всегда оказывается нулевой.
🌸 Как повторить
Вызвать uuid.NewV7().String() и собрать программу под js/wasm (например, GOOS=js GOARCH=wasm go run main.go через Node). В результате на свет стабильно появляются значения такого вида:
Обратите внимание на третий блок — он всегда 7000. Меняется только timestamp в начале и хвост, а вот этот фрагмент будто прибит гвоздями.
❓ Почему 7000 — это проблема
Чтобы понять, что именно сломано, полезно вспомнить структуру UUID версии 7. Внутри 128 бит лежат:
48 бит — Unix-таймстамп в миллисекундах; 4 бита — номер версии (0111, то есть 7); 12 бит — случайные данные (rand_a); 2 бита — вариант (10); 62 бита — случайные данные (rand_b).В строковом виде
019ee60f-29b3-7000-... блок 7000 — это как раз версия (7) плюс те самые 12 бит rand_a. И вот эти 12 бит вместо случайных стабильно равны нулю — 000.
Страдает именно 12-битное поле rand_a. Это сужает круг подозреваемых — где-то на пути заполнения этого конкретного куска случайными байтами на js/wasm что-то идёт не так.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoLivepackage main
import (
"log"
"github.com/crgimenes/glaze"
_ "github.com/crgimenes/glaze/embedded"
)
func main() {
w, err := glaze.New(true)
if err != nil {
log.Fatal(err)
}
defer w.Destroy()
w.SetTitle("Glaze")
w.SetSize(800, 600, glaze.HintNone)
w.SetHtml("<h1>Hello from Glaze</h1>")
w.Run()
}
🌸 Что делает его практичным: три хелпера
• BindMethods рефлексией пробегает по экспортируемым методам структуры и автоматически регистрирует их как JS-функции. Например, метод GetUserByID с префиксом api становится api_get_user_by_id. Это избавляет от ручного вызова Bind на каждый метод и даёт консистентный JS-API из обычного Go-сервиса.
• RenderHTML рендерит именованный Go-шаблон (html/template) в строку для SetHtml, включая вложенные шаблоны. Удобно, когда хочется серверный рендеринг шаблонов в локальном приложении без поднятия HTTP-сервера.
• AppWindow — пожалуй, самое интересное. Он оборачивает любой http.Handler в нативное окно, поднимая локальный loopback HTTP-сервер. Транспорт выбираемый: auto (unix-сокет на macOS/Linux, tcp на Windows), tcp или unix. На практике это значит, что готовое net/http-приложение можно почти без изменений превратить в десктопное — роутинг, шаблоны и ассеты остаются прежними.
Нативный WebView без CGo плюс возможность переиспользовать net/http-приложение как десктоп — выглядит очень аккуратно и практично.
➡️ Репозиторий
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoToProductionfunc startWorkers(jobs <-chan Job) {
for i := 0; i < 10; i++ {
go func() {
for job := range jobs {
process(job)
}
}()
}
}
Каждый воркер читает канал jobs и обрабатывает всё, что приходит. Чистый идиоматичный Go. Проблема в том, что если jobs никто не закрывает, все эти горутины блокируются навсегда.
Цикл for range по каналу блокируется, пока канал не закрыт или пока не пришло значение. Если продюсер перестал слать данные, но не вызвал close(jobs), ваши 10 воркеров висят в памяти бесконечно. Они держат стек и не видны в метриках, пока вы явно не считаете число горутин.
В долгоживущем сервисе это накапливается. Каждый раз, когда вы перезапускаете пул, например при перезагрузке конфига или новой пачке задач, не дренируя старый, вы добавляете утёкшие горутины.
➡️ Как чинить
Продюсер закрывает канал, когда закончил, и эта ответственность лежит только на нём.
func runBatch(jobList []Job) {
jobs := make(chan Job, len(jobList))
// Продюсер владеет каналом и закрывает его по завершении
go func() {
defer close(jobs) // сработает даже при панике
for _, job := range jobList {
jobs <- job
}
}()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
process(job)
}
}()
}
wg.Wait()
}
Здесь два важных момента:
• во-первых, defer close(jobs) в горутине продюсера закрывает канал, даже если продюсер вышел раньше времени или упал с паникой при отправке задач.
• во-вторых, sync.WaitGroup даёт чистую точку синхронизации. Вызов wg.Wait() блокируется, пока все воркеры не дочитают канал.
Продюсер владеет каналом и закрывает его, всегда через defer. Быстрая диагностика на проде такая. Добавьте runtime.NumGoroutine() в health-эндпоинт. Если это число растёт за время жизни сервиса и не возвращается обратно, у вас утечка. В тестах самый простой способ это многократно гонять пул и следить за ростом числа горутин через runtime.NumGoroutine().
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoToProduction
Вже доступно! Дослідження Telegram за 2025 — головні інсайти року 
