GoSeniorExperience

Какие 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/automaxprocs Go видит все 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 и безопасность
  • Умеет ранжировать риски по вероятности и влиянию

Related topics

Какие production-риски чаще всего возникают в проектах на Go (Golang): производительность, зависимости, конкурентность, деплой или observability? | Talanto