Почему ваш Go-код падает, и как это исправить без головной боли
Вы написали идеальный, по вашему мнению, код на Go. Компиляция прошла на ура, тесты зелёные... но на проде всё летит в тартарары. Знакомо? Добро пожаловать в реальный мир разработки, где баги — это не ошибки новичков, а часть профессионального роста. Сегодня мы разберём отладку Go-кода так, чтобы вы не просто находили ошибки, а предотвращали их появление на этапе написания.
Go — язык, который даёт разработчику мощные инструменты для отладки прямо «из коробки». Но многие используют только fmt.Println и искренне удивляются, почему их горутины ведут себя как кошки на раскалённой крыше. Давайте исправим это раз и навсегда.
Техника №1: Умный Print — когда log.Fatal не нужен
Да, я знаю, что вы скажете: «Зачем мне delve, если я могу напечатать переменную?» И будете правы в 80% случаев. Но проблема не в fmt.Println, а в том, как вы его используете.
Переходим от Println к structured logging
Вместо того чтобы лепить десятки принтов, используйте пакет log или slog (доступен с Go 1.21):
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
// Вместо fmt.Println("ошибка:", err)
if err := doSomething(); err != nil {
logger.Error("операция не удалась",
"error", err,
"module", "data_processor",
"attempt", 3,
)
}
}
Такой подход даёт вам структурированные логи, которые легко парсить, фильтровать и анализировать. На проде это спасёт вас от ночных звонков.
Лайфхак: условная компиляция для debug-логов
Чтобы не засорять продакшн, используйте build tags:
//go:build debug
package main
var debug = true
//go:build !debug
package main
var debug = false
Запускаете с флагом -tags debug — получаете подробные логи. Без флага — чистый продакшн.
Техника №2: Delve — ваш личный детектив для Go
Если print-отладка — это велосипед, то Delve — это Tesla. Этот отладчик создан специально для Go и понимает горутины, интерфейсы и даже generics. Установка простая:
go install github.com/go-delve/delve/cmd/dlv@latest
Точки остановки, которые умеют думать
Delve позволяет ставить не просто breakpoint, а условные точки:
(dlv) break main.go:25 if userID == 42
(dlv) continue
Отладчик остановится только когда userID будет равен 42. Представьте, сколько времени это экономит при отладке циклов.
Исследование горутин
Самое страшное в Go — deadlock'и и race condition. Delve показывает все активные горутины:
(dlv) goroutines
Goroutine 1 - User: ./main.go:10 main.main (0x109a5b6)
Goroutine 2 - User: ./main.go:15 main.worker (0x109a6c8)
* Goroutine 3 - User: ./main.go:22 main.worker (0x109a6c8)
Звёздочка показывает текущую горутину. Переключайтесь между ними:
(dlv) goroutine 2
(dlv) stack
Работа с памятью: смотрим, что внутри
Команда locals показывает все локальные переменные, а print выводит их значения с типами:
(dlv) locals
data = []int len: 100000, cap: 100000
result = map[string]int ["error": 5, "success": 99995]
(dlv) print data[0:5]
[]int len: 5, cap: 100000, [42, 17, 89, 3, 256]
Техника №3: Профилирование pprof — находим узкие места
Ваш код работает, но медленно? pprof — это встроенный профайлер Go. Он покажет, где программа «тормозит» и где ест память.
Добавляем HTTP-эндпоинт для профилирования
package main
import (
"net/http"
_ "net/http/pprof"
)
func main() {
// Запускаем pprof на отдельном порту
go func() {
http.ListenAndServe(":6060", nil)
}()
// Ваш основной код
select {}
}
Теперь открывайте в браузере http://localhost:6060/debug/pprof/ и смотрите:
- goroutine — сколько горутин и где они ждут
- heap — утечки памяти
- profile — CPU-профиль (30 секунд сбора данных)
Анализ CPU-профиля из командной строки
# Собираем профиль
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30
# Откроется веб-интерфейс с flame graph и графом вызовов
Если вы видите, что функция json.Unmarshal занимает 70% времени — это повод задуматься об оптимизации сериализации.
Техника №4: Tracing — как работает ваша программа на самом деле
go trace — это «рентген» вашего приложения. Он показывает не только какие функции вызываются, но и когда, и как долго выполняются горутины.
package main
import (
"os"
"runtime/trace"
)
func main() {
f, _ := os.Create("trace.out")
defer f.Close()
trace.Start(f)
defer trace.Stop()
// Ваш код
}
После запуска анализируем:
go tool trace trace.out
Откроется браузер с интерактивным таймлайном. Вы увидите:
- Когда горутины спят в ожидании каналов
- Сколько времени занимает GC
- Где происходят блокировки
Техника №5: Race detector — убийца гонок данных
Гонки данных (data races) — бич многопоточного программирования. Go встроил детектор прямо в компилятор:
go run -race main.go
go test -race ./...
Если есть гонка, вы получите чёткий отчёт:
==================
WARNING: DATA RACE
Write at 0