Traceback em Go: Como Explorar a Pilha de Execução

Muitas linguagens de programação fornecem algum tipo de “stack trace” ou “backtrace”. Python usa o módulo traceback, C no Linux oferece a função backtrace() em execinfo.h (não faz parte do padrão do C) e Java inclui Thread.dumpStack() ou e.printStackTrace() para exceções.

Stack Trace em Go

O Go fornece a função runtime.Caller para obter informações sobre a pilha de execução. O exemplo a seguir exibe o arquivo, a linha e a função que chamaram info(). Observe que o argumento 1 passado para runtime.Caller indica que queremos informações sobre a função que chamou info():

func info() {
    count := 1
    for {
        pc, filePath, lineNumber, ok := runtime.Caller(count)
        if !ok {
            return
        }

        funcName := runtime.FuncForPC(pc).Name()

        fmt.Printf("%d %s:%d %s\n",
            count,
            filePath,
            lineNumber,
            funcName)

        count++
    }
}

Você pode usar esses dados em um sistema de log para rastrear a origem de um erro. O package log padrão do Go já captura parte dessas informações, mas você pode querer personalizar a saída ou persistir os dados em um banco de dados. Em projetos com transações financeiras, por exemplo, é comum registrar cada transação com um ID único end-to-end que acompanha todas as etapas da transação e incluir alguns dados sobre a execução do programa torna muito mais fácil rastrear problemas e fazer auditorias.

Se você quiser imprimir a pilha de execução inteira, utilize um loop que chama runtime.Caller sucessivamente até não haver mais informações disponíveis. O exemplo a seguir ilustra essa abordagem:

func traceback() {
    i := 1
    for {
        _, file, line, ok := runtime.Caller(i)
        if !ok {
            break
        }
        fmt.Printf("File: %s, Line: %d\n", file, line)
        i++
    }
}

Assim, você obtém um “stack trace” personalizado em Go. Para mais detalhes sobre runtime.Caller, consulte a documentação oficial em: https://pkg.go.dev/runtime#Caller

runtime.CallersFrames

Outra forma de obter mais detalhes do programa é usar a função runtime.CallersFrames, que retorna um iterador para explorar a pilha de execução. O exemplo a seguir exibe o arquivo, a linha e a função de cada frame da pilha de execução:

pcs := make([]uintptr, 10)
n := runtime.Callers(0, pcs)
pcs = pcs[:n]

frames := runtime.CallersFrames(pcs)
for {
    frame, ok := frames.Next()
    if !ok {
        break
    }
    fmt.Printf(
        "%s:%d %s\n",
        frame.File,
        frame.Line,
        frame.Function,
    )
}

Log

Apenas para deixar registrado, você pode muito facilmente fazer com que o log do Go imprima a linha atual e o arquivo em que foi chamado, para isso basta usar o método SetFlags do log na inicialização do seu programa:

log.SetFlags(log.LstdFlags | log.Lshortfile)

-trimpath

O Go armazena o caminho completo dos arquivos do seu sistema e das dependências importadas. Se você compilar o programa na sua máquina, o caminho refletirá o diretório local, possivelmente expondo seu nome de usuário e outras informações irrelevantes ou sensíveis. Esses dados podem poluir seus registros de log. Para remover esses caminhos, use o parâmetro -trimpath ao compilar. Assim, o Go reduzirá a exposição de detalhes sensíveis:

go build -trimpath main.go

Conclusão

Obter a pilha de execução em Go é simples e ajuda a rastrear a origem de erros em projetos grandes. Esse recurso é uma ferramenta valiosa para auditoria e diagnóstico. Use runtime.Caller, a função de traceback personalizada ou runtime.CallersFrames para coletar informações detalhadas, e utilize parâmetros como -trimpath para proteger dados sensíveis.

Vídeo com a demonstração do código:

Cesar Gimenes

Última modificação