Какие production-риски чаще всего возникают в проектах на Go (Golang): производительность, зависимости, конкурентность, деплой или observability?
Главные production-риски Go: гонки данных без -race детектора, утечки горутин, зависание из-за блокировок без таймаутов, проблемы с зависимостями в go.sum и отсутствие structured logging и distributed tracing.
Конкурентность и гонки данных
Go упрощает написание конкурентного кода, но не защищает от гонок автоматически. Типичная ошибка — обращение к map из нескольких горутин без синхронизации:
// НЕПРАВИЛЬНО — гонка данных
var cache = map[string]string{}
func handler(w http.ResponseWriter, r *http.Request) {
cache[r.URL.Path] = "visited" // concurrent writes
}
// ПРАВИЛЬНО — sync.Map или RWMutex
var cache sync.Map
func handler(w http.ResponseWriter, r *http.Request) {
cache.Store(r.URL.Path, "visited")
}
Запускайте тесты и приложение с флагом -race:
// go test -race ./...
// go run -race main.go
Утечки горутин
Горутины дешевы, но не бесплатны. Запущенная горутина, ожидающая на заблокированном channel, живёт вечно:
// УТЕЧКА — никто никогда не пишет в ch
func leak() {
ch := make(chan int)
go func() {
v := <-ch // заблокирована навсегда
fmt.Println(v)
}()
}
// ПРАВИЛЬНО — используйте context для отмены
func noLeak(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case v := <-ch:
fmt.Println(v)
case <-ctx.Done():
return
}
}()
}
Мониторьте количество горутин через runtime.NumGoroutine() и метрику Prometheus go_goroutines.
Таймауты и дедлайны
HTTP-клиент без таймаута блокирует горутину навсегда при зависшем upstream:
// НЕПРАВИЛЬНО
client := &http.Client{} // нет таймаута
// ПРАВИЛЬНО
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 3 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 5 * time.Second,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
},
}
Зависимости и уязвимости
Используйте официальный vulnerability checker:
// Установка
// go install golang.org/x/vuln/cmd/govulncheck@latest
// Проверка
// govulncheck ./...
В CI добавьте go mod verify для проверки целостности go.sum.
Observability
Стандартный log пакет не поддаётся парсингу. Подключите zerolog или zap с обязательным request ID:
import "github.com/rs/zerolog/log"
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
logger := log.With().
Str("request_id", requestID).
Str("method", r.Method).
Str("path", r.URL.Path).
Logger()
ctx := logger.WithContext(r.Context())
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Деплой и graceful shutdown
srv := &http.Server{Addr: ":8080", Handler: router}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal().Err(err).Msg("server error")
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal().Err(err).Msg("forced shutdown")
}
Подводные камни
- Игнорирование возвращаемых ошибок — Go-конвенция предписывает обрабатывать каждую ошибку; используйте
errcheckлинтер в CI. - Closure захватывает переменную цикла — в Go 1.21 и ранее переменная цикла
for i, v := rangeзахватывается по ссылке; передавайте в горутину явно. - GOMAXPROCS в контейнере — без
uber-go/automaxprocsGo видит все CPU хоста, игнорируя cgroup-ограничения. - Паника без recover в горутине — паника в горутине не перехватывается внешним recover и падает весь процесс; добавляйте
defer func() { recover() }()в долгоживущие горутины. - Использование init() для инициализации зависимостей — порядок
init()между пакетами непредсказуем; инициализируйте все зависимости явно вmain(). - Отсутствие pprof в production — без
net/http/pprofна закрытом порту невозможно профилировать CPU и heap при инцидентах. - fmt.Errorf без %w — без
%wтеряется возможность использоватьerrors.Isиerrors.Asдля проверки типов ошибок. - Отсутствие linter в CI — запускайте
golangci-lint runс минимальным набором:errcheck,govet,staticcheck,gosec.
What hurts your answer
- Говорить только о запуске Go (Golang), но не об эксплуатации
- Не упоминать observability, обновления, безопасность и rollback
- Описывать риски абстрактно, без способов их снижать
What they're listening for
- Видит production-риски Go (Golang)
- Говорит про monitoring, rollout, rollback и безопасность
- Умеет ранжировать риски по вероятности и влиянию