console.time() / performance.now() → go test -bench
clinic doctor / 0x → go tool pprof
node --prof → runtime/pprof
process.memoryUsage() → runtime.ReadMemStats()
node --inspect → net/http/pprof (веб-интерфейс)
func BenchmarkXxx(b *testing.B)
b.ReportAllocs()net/http/pprofmkdir go-bench && cd go-bench
go mod init go-bench
# Устанавливаем зависимости
go get github.com/google/uuid
go mod tidy
# Создаём файлы
mkdir -p cmd/server
strings_bench_test.go — Бенчмаркиpackage main
import (
"fmt"
"strings"
"testing"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ БЕНЧМАРКИ (имя начинается с Benchmark) ║
// ╚══════════════════════════════════════════════════════════╝
// BenchmarkStringConcat — сравнение способов конкатенации строк
func BenchmarkStringConcat(b *testing.B) {
b.Run("PlusOperator", func(b *testing.B) {
b.ReportAllocs() // Отслеживаем аллокации
for i := 0; i < b.N; i++ {
s := ""
for j := 0; j < 100; j++ {
s += "x" // Много аллокаций!
}
}
})
b.Run("StringsBuilder", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.Grow(100) // Предвыделяем память
for j := 0; j < 100; j++ {
sb.WriteString("x")
}
_ = sb.String()
}
})
b.Run("StringsBuilderNoGrow", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j := 0; j < 100; j++ {
sb.WriteString("x")
}
_ = sb.String()
}
})
}
// BenchmarkSprintf — сравнение форматирования
func BenchmarkSprintf(b *testing.B) {
b.Run("Sprintf", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("Hello, %s! You are %d years old.", "Gopher", 13)
}
})
b.Run("Concat", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = "Hello, " + "Gopher" + "! You are " + fmt.Sprint(13) + " years old."
}
})
}
// BenchmarkSliceAllocation — сравнение способов создания слайсов
func BenchmarkSliceAllocation(b *testing.B) {
n := 10000
b.Run("MakeWithCap", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s := make([]int, 0, n) // Предвыделение
for j := 0; j < n; j++ {
s = append(s, j)
}
}
})
b.Run("MakeWithoutCap", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var s []int // Без capacity — реаллокации
for j := 0; j < n; j++ {
s = append(s, j)
}
}
})
b.Run("PreallocSlice", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s := make([]int, n) // Сразу нужного размера
for j := 0; j < n; j++ {
s[j] = j
}
}
})
}
// BenchmarkMapVsSlice — map vs slice для поиска
func BenchmarkMapVsSlice(b *testing.B) {
// Подготавливаем данные
keys := make([]string, 1000)
m := make(map[string]int, 1000)
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("key-%d", i)
keys[i] = key
m[key] = i
}
b.Run("MapLookup", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = m["key-500"]
}
})
b.Run("SliceLinearSearch", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, k := range keys {
if k == "key-500" {
break
}
}
}
})
}
json_bench_test.go — Бенчмарки JSONpackage main
import (
"encoding/json"
"testing"
)
type LargeStruct struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Tags []string `json:"tags"`
Metadata struct {
Region string `json:"region"`
Plan string `json:"plan"`
} `json:"metadata"`
}
var largeData = LargeStruct{
ID: 42,
Name: "Gopher",
Email: "gopher@golang.org",
Tags: []string{"go", "programming", "backend", "concurrency", "performance"},
Metadata: struct {
Region string `json:"region"`
Plan string `json:"plan"`
}{Region: "us-east-1", Plan: "premium"},
}
// BenchmarkJSONEncode — сравнение способов сериализации
func BenchmarkJSONEncode(b *testing.B) {
b.Run("Marshal", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
data, _ := json.Marshal(largeData)
_ = data
}
})
b.Run("Encoder", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var buf strings.Builder
enc := json.NewEncoder(&buf)
enc.Encode(largeData)
}
})
}
// BenchmarkJSONDecode — десериализация
func BenchmarkJSONDecode(b *testing.B) {
data, _ := json.Marshal(largeData)
b.Run("Unmarshal", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var v LargeStruct
json.Unmarshal(data, &v)
}
})
}
import "strings"
cmd/server/main.go — Сервер с pprofpackage main
import (
"fmt"
"log"
"net/http"
// Импорт pprof добавляет обработчики:
// /debug/pprof/
// /debug/pprof/heap
// /debug/pprof/goroutine
// /debug/pprof/profile?seconds=30
_ "net/http/pprof"
"runtime"
"sync"
"time"
)
// Утечка горутин (для демонстрации)
var leakedGoroutines sync.WaitGroup
// Утечка памяти (для демонстрации)
var globalCache = make(map[string][]byte)
func main() {
log.Println("🔍 Сервер с pprof на :8080")
// Эндпоинты для демонстрации проблем
http.HandleFunc("/api/leak-goroutines", leakGoroutinesHandler)
http.HandleFunc("/api/leak-memory", leakMemoryHandler)
http.HandleFunc("/api/cpu-load", cpuLoadHandler)
http.HandleFunc("/api/allocations", allocationsHandler)
http.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
// Логируем статистику каждые 10 секунд
go func() {
ticker := time.NewTicker(10 * time.Second)
for range ticker.C {
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("Горутин: %d | Heap: %.2f MB | Alloc: %.2f MB",
runtime.NumGoroutine(),
float64(m.HeapInuse)/1024/1024,
float64(m.Alloc)/1024/1024,
)
}
}()
log.Fatal(http.ListenAndServe(":8080", nil))
}
// leakGoroutinesHandler — создаёт утекающие горутины
func leakGoroutinesHandler(w http.ResponseWriter, r *http.Request) {
count := 10
fmt.Sscanf(r.URL.Query().Get("count"), "%d", &count)
for i := 0; i < count; i++ {
leakedGoroutines.Add(1)
go func(id int) {
// Горутина никогда не завершится — утечка!
<-make(chan struct{}) // Блокируется навсегда
leakedGoroutines.Done()
}(i)
}
fmt.Fprintf(w, "Создано %d утекающих горутин (всего: %d)\n",
count, runtime.NumGoroutine())
}
// leakMemoryHandler — создаёт утечку памяти
func leakMemoryHandler(w http.ResponseWriter, r *http.Request) {
// Выделяем 10 MB и сохраняем в глобальную переменную
data := make([]byte, 10*1024*1024) // 10 MB
key := fmt.Sprintf("leak-%d", time.Now().UnixNano())
globalCache[key] = data
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Fprintf(w, "Утечка 10 MB создана (ключ: %s)\n", key)
fmt.Fprintf(w, "Всего в кэше: %d записей\n", len(globalCache))
fmt.Fprintf(w, "Heap: %.2f MB\n", float64(m.HeapInuse)/1024/1024)
}
// cpuLoadHandler — создаёт нагрузку на CPU
func cpuLoadHandler(w http.ResponseWriter, r *http.Request) {
duration := 5 * time.Second
fmt.Sscanf(r.URL.Query().Get("duration"), "%v", &duration)
fmt.Fprintf(w, "Нагружаем CPU на %v...\n", duration)
// CPU-bound работа
done := make(chan struct{})
go func() {
for {
select {
case <-done:
return
default:
// Бесполезные вычисления
_ = fibonacci(40)
}
}
}()
time.Sleep(duration)
close(done)
fmt.Fprintf(w, "Готово!\n")
}
// allocationsHandler — создаёт много аллокаций
func allocationsHandler(w http.ResponseWriter, r *http.Request) {
count := 1_000_000
fmt.Sscanf(r.URL.Query().Get("count"), "%d", &count)
var data [][]byte
for i := 0; i < count; i++ {
// Каждый append создаёт новый слайс — много аллокаций
data = append(data, []byte(fmt.Sprintf("data-%d", i)))
}
fmt.Fprintf(w, "Создано %d слайсов\n", count)
}
// fibonacci — рекурсивный расчёт (неэффективно намеренно)
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
# ==========================================
# БЕНЧМАРКИ
# ==========================================
# Запуск всех бенчмарков
go test -bench=. -benchmem
# Запуск конкретного бенчмарка
go test -bench=BenchmarkStringConcat -benchmem
# Запуск на 5 секунд каждый
go test -bench=. -benchtime=5s -benchmem
# Сравнение (сохраняем результаты)
go test -bench=. -benchmem -count 5 > old.txt
# ... вносим изменения в код ...
go test -bench=. -benchmem -count 5 > new.txt
# Сравниваем
benchstat old.txt new.txt
# ==========================================
# ПРОФИЛИРОВАНИЕ (pprof)
# ==========================================
# Запускаем сервер
go run ./cmd/server/main.go
# В другом терминале:
# Смотрим доступные профили
curl http://localhost:8080/debug/pprof/
# CPU-профиль (30 секунд)
curl "http://localhost:8080/debug/pprof/profile?seconds=30" > cpu.prof
# Heap-профиль
curl http://localhost:8080/debug/pprof/heap > heap.prof
# Горутины
curl http://localhost:8080/debug/pprof/goroutine > goroutine.prof
# Создаём нагрузку перед профилированием
curl "http://localhost:8080/api/cpu-load?duration=30s" &
curl "http://localhost:8080/api/leak-memory"
curl "http://localhost:8080/api/leak-goroutines?count=100"
# Анализируем профили
go tool pprof cpu.prof
# В интерактивном режиме:
# top — топ функций по CPU
# list Func — код функции с профилем
# web — граф вызовов (нужен graphviz)
# flame — flame graph (нужен браузер)
# Веб-интерфейс для анализа
go tool pprof -http=:8081 cpu.prof
# Сравнение двух профилей
go tool pprof -http=:8081 -diff_base=old.prof new.prof
# Профилирование в продакшене (одноразово)
curl "http://production-server:8080/debug/pprof/profile?seconds=30" > prod-cpu.prof
go tool pprof -http=:8081 prod-cpu.prof
| Профиль | Эндпоинт | Что показывает | Когда использовать |
|---|---|---|---|
| CPU | /debug/pprof/profile | Где тратится процессорное время | Медленные запросы |
| Heap | /debug/pprof/heap | Распределение памяти | Утечки памяти, большое потребление |
| Goroutine | /debug/pprof/goroutine | Стек всех горутин | Утечки горутин, дедлоки |
| Allocs | /debug/pprof/allocs | Все аллокации (с момента старта) | Избыточные аллокации |
| Mutex | /debug/pprof/mutex | Блокировки мьютексов | Конкуренция за блокировки |
| Block | /debug/pprof/block | Блокировки на каналах | Проблемы с каналами |
| Флаг | Пример | Описание |
|---|---|---|
-bench | -bench=. | Запуск бенчмарков (регулярное выражение) |
-benchmem | -benchmem | Показывать аллокации и байты |
-benchtime | -benchtime=10s | Длительность бенчмарка |
-count | -count=5 | Количество повторений |
-cpuprofile | -cpuprofile=cpu.prof | Сохранить CPU-профиль |
-memprofile | -memprofile=mem.prof | Сохранить heap-профиль |
/debug/pprof доступен всем. Используйте firewall или auth middleware.
_ или глобальную переменную.💡 Best practices от сеньоров:
go tool pprof -http=:8081 показывает интерактивный flame graph.💡 Для Node.js разработчика:
runtime.ReadMemStats() — аналог process.memoryUsage().