Avatar ·

Как получить стек вызовов (stacktrace) всех горутин в Go для отладки

Привет! Я пишу многопоточное приложение на Go, которое использует много горутин. В какой-то момент программа начинает работать нестабильно — то зависает, то выдает странные ошибки. Я подозреваю, что проблема в deadlock (взаимной блокировке) или в утечке горутин (когда они создаются, но не завершаются). Мне нужно понять, какие горутины сейчас работают и на каких строках кода они остановились. Я знаю, что в Java можно использовать `jstack` или `Thread.getAllStackTraces()`, но в Go я не нашел аналога. Например, у меня есть такой код: ```go package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() time.Sleep(10 * time.Second) fmt.Println("goroutine 1 done") }() go func() { defer wg.Done() time.Sleep(5 * time.Second) fmt.Println("goroutine 2 done") }() wg.Wait() } ``` Как мне в рантайме (или при панике) вывести полный стек вызовов для всех активных горутин? Есть ли встроенные средства или нужно писать свою обертку? Хотелось бы увидеть не только сообщение об ошибке, но и номера строк, файлы и состояние каждой горутины. Заранее спасибо! <<>>

Стандартный способ: обработчик сигнала + runtime.Stack

В Go нет единой команды как jstack, но стандартная библиотека предоставляет мощный инструмент — пакет runtime. Основная функция — runtime.Stack(), которая возвращает стек вызовов всех (или текущей) горутин в виде строки. Самый популярный подход — повесить обработчик на сигнал SIGQUIT (или Ctrl+\ на Linux/macOS) или на SIGABRT. При получении сигнала программа автоматически выводит дамп всех горутин в stderr и завершается.

package main

import (
    "fmt"
    "os"
    "os/signal"
    "runtime"
    "syscall"
)

func main() {
    // Канал для получения сигналов
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGQUIT, syscall.SIGABRT)

    // Горутина-обработчик сигналов
    go func() {
        <-sigs
        // Буфер для стектрейса (обычно хватает 64 КБ на горутину)
        buf := make([]byte, 1<<20) // 1 MB
        n := runtime.Stack(buf, true) // true = все горутины
        fmt.Fprintf(os.Stderr, "=== goroutine dump ===\n%s\n", buf[:n])
        os.Exit(1)
    }()

    // Ваш код с горутинами
    fmt.Println("Запуск. Нажмите Ctrl+\\ для дампа")
    select {} // блокируем main
}

Если запустить такую программу и нажать Ctrl+\, в stderr появится примерно такой вывод:

goroutine 1 [select (no cases)]:
main.main()
        /home/user/main.go:23 +0x105

goroutine 5 [syscall]:
os/signal.signal_recv()
        /usr/local/go/src/runtime/sigqueue.go:152 +0x2e
os/signal.loop()
        /usr/local/go/src/os/signal/signal_unix.go:23 +0x22
created by os/signal.Notify
        /usr/local/go/src/os/signal/signal.go:128 +0x4a

Встроенный дамп при панике

Если ваша программа паникует (например, из-за разыменования nil-указателя или дедлока), Go по умолчанию выводит стектрейс всех горутин. Это работает "из коробки". Пример:

package main

func main() {
    ch := make(chan int)
    go func() {
        <-ch // заблокируется навсегда
    }()
    // main завершится, но горутина останется
    // panic не будет, но если вызвать panic, увидим все
    panic("test")
}

Вывод паники покажет все горутины, включая заблокированную.

Программный дамп без завершения

Если вы не хотите убивать программу, а просто хотите получить дамп в лог, используйте тот же runtime.Stack в любой момент:

func dumpGoroutines() string {
    buf := make([]byte, 1<<20)
    n := runtime.Stack(buf, true)
    return string(buf[:n])
}

Можно вызывать эту функцию по таймеру или при определённых условиях.

Продвинутый подход: net/http/pprof

Самый удобный способ для production — подключить пакет net/http/pprof. Он добавляет эндпоинт /debug/pprof/goroutine, который отдаёт стектрейсы в формате текста или protobuf.

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

func main() {
    go func() {
        http.ListenAndServe("localhost:6060", nil)
    }()
    // ... ваш код
}

Затем выполните curl http://localhost:6060/debug/pprof/goroutine?debug=2 — получите полный дамп.

Рекомендация

  • Для отладки на локальной машине — используйте SIGQUIT (Ctrl+\).
  • Для production — pprof или запись дампа в лог по расписанию.
  • Не забывайте про go tool pprof — он умеет анализировать дампы и показывать "горячие" горутины.
<<>>

Альтернатива: использование переменной среды GOTRACEBACK

Если вам нужно получить дамп всех горутин при краше без изменения кода, установите переменную окружения GOTRACEBACK=all. По умолчанию Go выводит стектрейс только для упавшей горутины, но с этим флагом — для всех.

GOTRACEBACK=all ./myapp

Если программа упадёт, вы увидите полный дамп. Для постоянного включения в production можно установить эту переменную в Dockerfile или systemd unit.

Дополнительный инструмент: delve (dlv)

Отладчик delve позволяет в рантайме получить дамп горутин. Подключитесь к процессу:

dlv attach <PID>
(dlv) goroutines
(dlv) goroutine 1 stack

Это даёт интерактивный доступ, но требует установки delve и прав на отладку.

Совет по утечкам

Для обнаружения утечек горутин (когда они создаются, но не завершаются) используйте пакет runtime/pprof с записью профиля в файл:

import "runtime/pprof"

f, _ := os.Create("goroutine.prof")
pprof.Lookup("goroutine").WriteTo(f, 2)
f.Close()

Затем анализируйте через go tool pprof goroutine.prof.