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

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

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

Начать курс

Введение: почему профилирование Go кода — это must have

Разработка на Go (Golang) славится своей производительностью и эффективностью. Однако даже самый быстрый код может тормозить, если не учитывать особенности работы с памятью, горутинами или вводом-выводом. Профилирование — это процесс сбора данных о выполнении программы, который позволяет заглянуть «под капот» и понять, какие функции потребляют больше всего CPU, памяти или времени.

В этой статье мы разберем практические инструменты профилирования в Go: pprof, trace и встроенные бенчмарки. Вы узнаете, как находить узкие места и оптимизировать код на реальных примерах.

1. Основные инструменты профилирования в Golang

Go предоставляет мощный арсенал для профилирования прямо из коробки. Рассмотрим три ключевых подхода:

  • pprof — профилирование CPU, памяти, блокировок и горутин.
  • trace — трассировка событий: создание горутин, системные вызовы, GC.
  • benchmark — встроенное тестирование производительности с помощью go test -bench.

Для начала работы с pprof достаточно импортировать net/http/pprof в ваш HTTP-сервер или использовать runtime/pprof для CLI-приложений.

1.1. Как запустить CPU профилирование

Самый простой способ — добавить эндпоинт в ваш HTTP-сервер:

package main

import (
    "log"
    "net/http"
    _ "net/http/pprof"
)

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    // ваш основной код
    select {}
}

После запуска откройте http://localhost:6060/debug/pprof/ — вы увидите дашборд. Чтобы собрать CPU профиль на 30 секунд, выполните:

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

Это загрузит данные и откроет интерактивную консоль pprof.

2. Находим узкие места с pprof: CPU и память

Предположим, у нас есть функция, которая обрабатывает большой массив данных. Мы подозреваем, что она тормозит. Давайте напишем тестовый пример и профилируем его.

2.1. Пример медленного кода

package main

import (
    "fmt"
    "time"
)

func slowFunction(data []int) int {
    sum := 0
    for _, v := range data {
        time.Sleep(1 * time.Microsecond) // имитация работы
        sum += v * v
    }
    return sum
}

func main() {
    data := make([]int, 100000)
    for i := range data {
        data[i] = i
    }
    result := slowFunction(data)
    fmt.Println(result)
}

Запустим CPU профилирование:

go run main.go &
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=10

В браузере откроется визуализация — граф вызовов (call graph). Толщина ребер показывает, сколько времени заняла каждая функция. Мы сразу увидим, что slowFunction и time.Sleep — главные потребители CPU.

2.2. Анализ памяти (heap profile)

Чтобы найти утечки памяти или объекты, которые занимают слишком много места, используйте:

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

В интерактивной консоли можно выполнить команду top:

(pprof) top10
Showing nodes accounting for 80MB, 100% of 80MB total
      flat  flat%   sum%        cum   cum%
      40MB 50.00% 50.00%       40MB 50.00%  main.slowFunction
      20MB 25.00% 75.00%       20MB 25.00%  main.createLargeSlice
      20MB 25.00% 100.00%      20MB 25.00%  runtime.makeslice

Это показывает, какие функции аллоцируют больше всего памяти. Если вы видите, что main.slowFunction выделяет 40MB, стоит задуматься о рефакторинге — например, использовать повторное использование слайсов.

3. Трассировка (trace): находим проблемы с горутинами и блокировками

Иногда программа работает медленно не из-за CPU, а из-за блокировок, ожидания каналов или GC. trace помогает увидеть временную шкалу событий.

3.1. Как собрать trace

package main

import (
    "os"
    "runtime/trace"
)

func main() {
    f, _ := os.Create("trace.out")
    defer f.Close()
    trace.Start(f)
    defer trace.Stop()

    // ваш код
    ch := make(chan int)
    go func() {
        ch <- 42
    }()
    <-ch
}

Затем анализируем:

go tool trace trace.out

Откроется веб-интерфейс с временной шкалой. Вы увидите:

  • Когда стартуют и заканчиваются горутины.
  • Продолжительность GC (Garbage Collection).
  • Блокировки на мьютексах и каналах.

Если вы видите, что горутина подолгу ждет в канале — возможно, стоит увеличить буфер или пересмотреть архитектуру.

4. Бенчмарки: профилирование без сервера

Для изолированного тестирования функций используйте testing.B. Это идеально для сравнения реализаций.

4.1. Пример бенчмарка

Создайте файл main_test.go:

package main

import "testing"

func BenchmarkSlowFunction(b *testing.B) {
    data := make([]int, 100000)
    for i := range data {
        data[i] = i
    }
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        slowFunction(data)
    }
}

Запустите:

go test -bench=. -benchmem

Результат:

BenchmarkSlowFunction-8   100   10523456 ns/op   800000 B/op   100000 allocs/op

Параметр <

Блоги

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