Профилирование Go кода: находим узкие места для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Профилирование Go кода: находим узкие места для начинающих

Когда ваш Go-проект начинает тормозить, потреблять слишком много памяти или работать нестабильно, возникает закономерный вопрос: "Где узкое место?" Интуитивные догадки редко помогают — нужны точные данные. Профилирование (profiling) — это процесс сбора метрик о работе программы: сколько времени занимает каждая функция, сколько памяти выделяется, как часто вызывается сборщик мусора. В этой статье мы разберем, как с помощью встроенных инструментов Go (pprof и trace) быстро находить и устранять проблемы производительности.

Что такое профилирование и зачем оно нужно?

Профилирование — это как рентген для вашего кода. Вы видите не просто "программа работает медленно", а конкретные цифры: функция parseData() занимает 40% времени, а loadConfig() выделяет 2 ГБ памяти. Для Go существует несколько типов профилей:

  • CPU профиль — показывает, какие функции потребляют процессорное время.
  • Memory (heap) профиль — показывает, какие функции выделяют память и где живут объекты.
  • Goroutine профиль — показывает количество активных горутин и их состояние.
  • Block профиль — показывает, где горутины блокируются на мьютексах или каналах.
  • Mutex профиль — показывает конкуренцию за мьютексы.

Для начинающих самым полезным будет CPU и Memory профилирование. Они дают 80% ответов на вопрос "почему код тормозит".

Инструмент pprof: встроенный профилировщик Go

Go поставляется со встроенным пакетом net/http/pprof. Он добавляет эндпоинты к вашему HTTP-серверу, с которых можно снимать профили. Рассмотрим базовый пример.

Шаг 1: Подключаем pprof к приложению

package main

import (
    "fmt"
    "log"
    "net/http"
    _ "net/http/pprof" // подключаем профилировщик
    "time"
)

func main() {
    // Запускаем сервер на порту 8080
    go func() {
        log.Println(http.ListenAndServe("localhost:8080", nil))
    }()

    // Наша "тяжелая" бизнес-логика
    for i := 0; i < 100; i++ {
        go heavyTask(i)
    }

    // Ждем завершения
    time.Sleep(10 * time.Second)
    fmt.Println("Done")
}

func heavyTask(id int) {
    // Симуляция тяжелой работы
    for i := 0; i < 1000000; i++ {
        _ = i * i
    }
    fmt.Printf("Task %d finished\n", id)
}

После запуска приложения откройте браузер по адресу http://localhost:8080/debug/pprof/. Вы увидите веб-интерфейс со ссылками на профили.

Шаг 2: Снимаем CPU профиль

Самый простой способ — использовать командную строку. Запустите:

go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

Эта команда соберет CPU профиль в течение 30 секунд. После завершения откроется интерактивная консоль pprof. Введите top чтобы увидеть самые "горячие" функции:

(pprof) top
Showing nodes accounting for 1.50s, 100% of 1.50s total
      flat  flat%   sum%        cum   cum%
     1.50s   100%   100%      1.50s   100%  main.heavyTask
         0     0%   100%      1.50s   100%  runtime.main

Здесь flat — время, потраченное непосредственно внутри функции, а cum — время с учетом вызовов дочерних функций. В нашем примере 100% времени уходит на heavyTask.

Шаг 3: Визуализация через веб-интерфейс

Для более наглядного анализа используйте графический режим:

go tool pprof -http=:8081 http://localhost:8080/debug/pprof/profile?seconds=30

Откроется браузер с интерактивным графом вызовов. Чем больше и краснее блок — тем больше ресурсов он потребляет. Это отличный способ быстро найти "жирные" функции.

Профилирование памяти: находим утечки

Утечки памяти в Go — это обычно не забытые освобождения (как в C), а удерживаемые ссылки на объекты, которые сборщик мусора не может собрать. pprof помогает найти такие места.

Снимаем heap профиль

go tool pprof http://localhost:8080/debug/pprof/heap

В консоли pprof введите top чтобы увидеть функции, выделяющие больше всего памяти:

(pprof) top
Showing nodes accounting for 512MB, 100% of 512MB total
      flat  flat%   sum%        cum   cum%
   512MB   100%   100%     512MB   100%  main.createLargeSlice

Теперь давайте посмотрим на пример с потенциальной утечкой:

package main

import (
    "fmt"
    "net/http"
    _ "net/http/pprof"
    "time"
)

var cache = make(map[string][]byte)

func main() {
    go func() {
        http.ListenAndServe("localhost:8080", nil)
    }()

    // Симуляция утечки: добавляем данные в глобальный кэш без удаления
    for i := 0; i < 1000; i++ {
        go func(n int) {
            key := fmt.Sprintf("key-%d", n)
            data := make([]byte, 10*1024*1024) // 10 MB
            cache[key] = data
            time.Sleep(1 * time.Hour) // никогда не удаляем
        }(i)
    }

    time.Sleep(10 * time.Minute)
}

Сняв heap профиль, вы увидите, что main функция удерживает 10 ГБ памяти. В реальном проекте вы бы увидели, что cache растет бесконтрольно. Решение — добавить TTL (time-to-live) или использовать sync.Map с очисткой.

Трассировка: видим горутины и события

Иногда профилирования CPU и памяти недостаточно — нужно понять, как горутины взаимодействуют друг с другом. Для этого используется трассировка (trace). Она показывает временную шкалу: когда стартовали и завершились горутины, как работали каналы и мьютексы, когда

Блоги

Рекомендаций по книгам