Введение: почему профилирование 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
Параметр <