1. Карта экосистемы Go
Когда говорят «экосистема Go», легко представить себе рынок со специями: тут роутеры, там логгеры, вон там ORM на любой вкус и цвет. Но полезнее держать в голове более скучную, зато рабочую картину: у нас есть стандартная библиотека, есть «полу-официальные» пакеты (например, golang.org/x/...), и есть внешние библиотеки от сообщества. И это нормально: Go не пытается включить в стандартную поставку всё, что только можно, но при этом даёт очень мощный базовый набор.
Важная мысль для начинающего разработчика такая: вы не обязаны знать все библиотеки. Вам достаточно понимать, какие задачи вообще бывают (CLI, HTTP, конфиг, логи, БД, тесты…), что покрывает стандартная библиотека, и как оценивать цену внешней зависимости. Это как с инструментами на кухне: базовый нож и доска решают 80% задач, а вот «ультразвуковой нож для помидоров» стоит покупать только если вы реально режете помидоры на поток.
Стандартная библиотека: ваш первый рюкзак
Стандартная библиотека Go — это не «минимальный набор, чтобы компилировалось», а очень практичная база, на которой действительно можно строить приложения. Причём эта база развивается: иногда появляются новые пакеты, которые закрывают то, что раньше приходилось решать вручную или через сторонние решения. Например, в Go 1.21 добавили log/slog для структурного логирования и пакеты slices, maps, cmp, которые упрощают работу с типовыми операциями.
Полезно воспринимать стандартную библиотеку как «набор проверенных временем кирпичей». Они не всегда самые модные, не всегда самые удобные в один вызов, но они обычно стабильные, предсказуемые, документированные и поддерживаются командой Go. А ещё они почти всегда дают вам правильные абстракции, которые потом легко тестировать и сопровождать: io.Reader/io.Writer, context.Context, net/http, encoding/json, database/sql (как интерфейс к SQL-драйверам) и так далее.
Чтобы не превращать лекцию в справочник, давайте зафиксируем это в виде таблицы «что обычно нужно приложению» и «чем отвечает stdlib».
| Задача в приложении | Стандартная библиотека (первый кандидат) | Комментарий “как думать” |
|---|---|---|
| CLI-аргументы и флаги | |
Просто, достаточно для большинства утилит |
| HTTP сервер | |
База, на которой работают и многие фреймворки |
| URL и query params | |
Безопаснее, чем “склеить строку” |
| JSON | |
Не самый быстрый, но стандарт де-факто |
| Логи | |
slog — нормальная структура и уровни |
| Тесты | |
Сильная база, особенно с table-driven стилем |
| Работа с временем | |
Таймауты, дедлайны, форматирование |
| Файлы/FS | |
Большая часть практики закрывается ими |
Мини-пример в духе нашего tasker: структурный логгер из стандартной библиотеки — это уже не «одна строка на всё», а нормальные поля.
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("task created", "id", 42, "title", "buy milk")
// task created id=42 title="buy milk"
}
Обратите внимание на приятную вещь: мы ничего не «подключали», не спорили про «лучший логгер 2026 года», а уже получили полезный формат. Именно так Go часто и работает: сначала берём stdlib, и очень часто на этом можно остановиться.
2. Когда stdlib — лучший выбор
Новички иногда воспринимают выбор стандартной библиотеки как «я просто не знаю нормальные библиотеки». На практике у опытных Go-разработчиков часто наоборот: stdlib — это осознанный выбор, потому что она снижает число движущихся частей. Чем меньше движущихся частей — тем меньше мест, где всё может сломаться в пятницу вечером (а пятница, как известно, специально создана для падений продакшена).
Есть простой мысленный тест: если вы можете решить задачу на стандартной библиотеке за разумное время и с понятным кодом, то stdlib почти всегда выигрывает. Вы экономите на чтении документации, на обновлениях, на транзитивных зависимостях и на сюрпризах вида «в новой версии поменяли поведение по умолчанию». И главное: вы уменьшаете «площадь поверхности» проекта — меньше чужого кода, который вы не контролируете.
Для нашего tasker это очень хорошо видно на HTTP-слое. Базовый сервер на net/http выглядит просто и прозрачно:
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
_ = http.ListenAndServe(":8080", mux)
}
И да: это уже похоже на «настоящее приложение». Можно добавлять middleware (в Go это часто просто обёртки вокруг handler’ов), можно подключать логирование, можно делать JSON-ответы — и всё это будет оставаться читаемым.
Отдельный бонус stdlib — её эволюция. То, что раньше делали вручную или через сторонние пакеты, постепенно появляется «официально», если это массовая потребность. Например, семейство функций для слайсов в пакете slices развивалось так, чтобы снижать типовые ошибки, включая работу с «хвостами» и освобождением ссылок через clear. Это хороший пример того, как Go старается упрощать жизнь без превращения языка в «зоопарк магии».
4. Зачем нужны внешние библиотеки
Если стандартная библиотека такая классная, зачем вообще нужны внешние пакеты? Потому что мир всегда сложнее, чем базовый набор. Иногда вам нужно больше удобства (developer experience), иногда — более высокая производительность на горячем пути, иногда — готовые интеграции (например, конкретная база данных, конкретная система метрик), а иногда — просто более удачный API для вашей команды.
Самая важная рамка: внешняя библиотека — это не только функциональность, но и обязательства. Вы берёте чужой код в свой проект, а значит, соглашаетесь жить с его обновлениями, возможными уязвимостями, изменениями API и даже с тем, что автор может однажды «устать» и бросить поддержку. Поэтому правильная логика звучит так: «мы берём зависимость, потому что она даёт измеримую пользу, и мы понимаем её цену».
Есть и второй аспект: Go-проекты часто строят архитектуру так, чтобы внешние библиотеки жили на границах (в адаптерах), а ядро (домен и usecases) оставалось максимально простым. Тогда вы можете заменить библиотеку, не переписывая всё приложение. Это мы уже делали концептуально, когда отделяли CLI/HTTP от бизнес-логики и когда «маппили» доменные ошибки в exit codes или HTTP-статусы.
5. Ориентиры по популярным библиотекам
Сейчас будет важная оговорка: популярность библиотек меняется, и задача этой лекции — не выдать вам «список единственно правильных пакетов», а дать карту местности. То есть: какие категории бывают и какие имена вы будете регулярно встречать в реальных репозиториях. Даже если вы не выберете эти библиотеки, вы хотя бы не будете смотреть на них как на инопланетян.
Ниже — практичная таблица: «задача», «stdlib-альтернатива», «часто встречающиеся внешние варианты» (именно как ориентиры).
| Категория | Что есть в stdlib | Что часто используют снаружи (ориентиры) | Когда это бывает оправдано |
|---|---|---|---|
| CLI-команды/подкоманды | |
|
Когда много подкоманд, автогенерация help, сложный UX |
| HTTP-роутинг | |
chi, gin, echo, (иногда старые gorilla/mux) | Когда нужен удобный роутинг, группы middleware, «красивые» параметры |
| Конфиг | |
|
Когда много настроек, хочется единый слой «flags/env/file» |
| Логирование | |
|
Когда важна скорость/формат/интеграции, но stdlib уже часто хватает |
| Валидация структур | руками (if’ами) | |
Когда много DTO и правил валидации, особенно на границах (HTTP/JSON) |
| Работа с SQL | |
pgx (как драйвер/клиент), sqlx, ORM типа gorm | Когда нужны удобные сканы/маппинг, но важно не потерять контроль |
| Миграции БД | (нет) | |
Когда проект с БД и нужна дисциплина схем |
| Тест-ассёрты и моки | |
|
Когда хочется короче тесты, но важно не сделать их «магическими» |
| Метрики/трейсинг | (нет «полного набора») | |
Когда сервис живёт в проде и нужен наблюдаемый контракт |
| UUID | (базового генератора «как везде» нет) | |
Когда формат ID должен быть стандартным |
Как этим пользоваться на практике? Очень просто: когда вы видите в чужом проекте chi, вы понимаете «это роутер поверх net/http»; когда видите cobra, вы понимаете «это комбайн для CLI»; когда видите pgx, вы понимаете «это про PostgreSQL». И вы можете читать код без паники (а паника, напоминаю, по курсу у нас вообще не для всего подряд).
6. Цена зависимости: граф, обновления и поддержка
Когда вы добавляете зависимость, вы добавляете не только строчку в go.mod, но и целый хвост. Потому что зависимости бывают прямые (те, что вы импортируете), и транзитивные (те, что импортируют ваши зависимости). В итоге маленький «удобный пакетик» может притащить десятки других модулей, а вместе с ними — обновления, CVE, несовместимости и сюрпризы.
Go Modules как раз устроены так, чтобы зависимостями можно было управлять дисциплинированно. Команды вроде go get и go mod tidy меняют go.mod и go.sum, и это ожидаемое поведение: инструмент поддерживает проект в согласованном состоянии. А ещё важно помнить про установку инструментов фиксированной версии через go install module@version, чтобы «вчера работало, сегодня тоже работает», а не «у меня на ноутбуке магия».
Полезно иногда рисовать себе простую картину зависимостей. Например, наш tasker (упрощённо) выглядит так:
flowchart TD
cmd[cmd/tasker/main.go] --> cli[adapters/cli]
cmd --> http[adapters/http]
cli --> app[app/usecases]
http --> app
app --> domain[domain/model]
app --> storage[adapters/storage]
cli -.-> std[stdlib: flag, fmt, os]
http -.-> std2[stdlib: net/http, encoding/json]
Если вы добавляете, скажем, внешний роутер, он должен сидеть внутри adapters/http, а не протекать в domain и app. Тогда цена зависимости контролируемая: вы можете её поменять, не вырывая полдома вместе с фундаментом.
7. Как выбрать библиотеку: чек-лист мышления
Проблема выбора библиотек у новичков обычно не в том, что они «плохо выбирают», а в том, что они выбирают по неправильным сигналам. Самый частый неправильный сигнал — «в туториале так сделали». Второй по популярности — «у этой библиотеки много звёзд». Звёзды полезны, но они не гарантируют, что библиотека подходит именно вам и именно в ваш контекст.
Вместо этого держите более инженерный сценарий мышления. Сначала вы формулируете, что именно вам нужно: роутинг с параметрами? middleware chaining? генерация help в CLI? структурные логи JSON? Затем вы проверяете, решает ли это stdlib достаточно хорошо. Если да — вы экономите месяцы будущей жизни. Если нет — вы выбираете внешнюю библиотеку, но так, чтобы она не расползлась по проекту.
Дальше вы оцениваете «жизнеспособность» зависимости. Поддерживается ли проект, есть ли релизы, понятна ли документация, есть ли тесты, насколько тяжёлый dependency graph, насколько агрессивные breaking changes. И — очень важно — насколько библиотека дружит с подходом Go «простое API, явные ошибки, минимум магии». Иногда «слишком умный» фреймворк делает код короче, но мышление команды дороже. А Go — язык, который обычно покупают именно ради дешёвого мышления.
8. Подключаем внешнее аккуратно: через адаптеры
В этом разделе мы привяжемся к нашему tasker и покажем главный практический приём: мы не «используем библиотеку везде», мы делаем адаптер. И тогда внешняя библиотека становится заменяемой деталью, а не частью ДНК приложения.
Представим, что у нас есть доменный уровень, который вообще не знает про HTTP и CLI:
package domain
type Task struct {
ID int
Title string
Done bool
}
А в app-слое мы зависим от интерфейсов, а не от конкретных библиотек:
package app
import "context"
type Task struct {
ID int
Title string
Done bool
}
type Storage interface {
Create(ctx context.Context, title string) (Task, error)
}
Теперь HTTP-адаптер может быть на чистом net/http, а может быть на внешнем роутере — но app об этом не узнаёт.
Точно так же можно поступить с логированием: внутри приложения вы можете зависеть от очень маленького интерфейса, а снаружи подключать хоть log/slog, хоть другой логгер.
package app
type Logger interface {
Info(msg string, args ...any)
Error(msg string, args ...any)
}
И адаптер на slog будет выглядеть максимально скучно (а скучно — это комплимент):
package adapters
import "log/slog"
type SlogLogger struct {
L *slog.Logger
}
func (l SlogLogger) Info(msg string, args ...any) { l.L.Info(msg, args...) }
func (l SlogLogger) Error(msg string, args ...any) { l.L.Error(msg, args...) }
Почему это круто? Потому что если через год вы решите «нам нужен другой логгер» — вы перепишете один файл адаптера, а не весь проект. И это ровно та самая «упаковка результата», о которой мы говорили на финише курса: качество — это не только «работает», но и «меняется без боли».
9. Типичные ошибки при выборе stdlib vs внешних библиотек
Ошибка №1: брать внешнюю библиотеку «на всякий случай».
Это выглядит безобидно: «пусть будет cobra, вдруг пригодится». Проблема в том, что зависимость начинает жить своей жизнью: обновления, несовместимости, транзитивные модули. Если вы реально не используете возможности библиотеки, вы платите за неё просто фактом её существования.
Ошибка №2: тащить библиотеку в домен и usecases.
Самый дорогой сценарий — когда domain начинает импортировать что-то внешнее (роутер, логгер, ORM-модель с тегами, HTTP-типы). Тогда библиотека перестаёт быть деталью, и становится архитектурным решением «навсегда». Гораздо дешевле держать всё внешнее на границе: CLI/HTTP/storage/adapters.
Ошибка №3: путать «удобнее писать» и «дешевле сопровождать».
Некоторые библиотеки действительно позволяют написать меньше кода сегодня. Но сопровождение — это про чтение, тестирование, обновления и дебаг через год. В Go выгодно писать чуть более многословно, но предсказуемо. Это как переписка: можно сократить слова до эмодзи, но потом сам же не поймёшь, что имел в виду.
Ошибка №4: выбирать по хайпу, а не по требованиям.
«Все используют X» — слабый аргумент, пока вы не понимаете, какую проблему решает X. Правильный аргумент звучит так: «нам нужно A, B, C; stdlib закрывает A и B, но не закрывает C; библиотека X закрывает C и не ломает нам архитектуру».
Ошибка №5: забывать про дисциплину модулей и версий инструментов.
Если в команде «у каждого свои версии тулов», вы получаете чудеса вида «у меня тесты проходят, у тебя нет». В Go принято фиксировать зависимости и поддерживать порядок через go mod tidy, а инструменты ставить версионно (через go install ...@version). Это снижает случайность и делает проект воспроизводимым.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ