Errors sinalizam falhas esperadas — condições que quem chama deve antecipar e tratar. Panic é diferente. Um panic sinaliza que o programa entrou em um estado do qual não consegue se recuperar por meios normais: um invariante foi violado, uma condição impossível foi atingida ou o runtime detectou algo fundamentalmente errado. Entender quando Go lança panics, como eles se propagam pela call stack, e o que recover pode e não pode fazer molda a forma como você escreve programas robustos e projeta limites seguros entre packages.
O que é um panic
Um panic é uma interrupção abrupta da execução normal de uma goroutine. O runtime Go lança panics automaticamente para um conjunto restrito de violações: acessar um índice de slice ou array fora dos limites, desreferenciar um nil pointer, dividir um inteiro por zero, ou escrever em um channel fechado.
Executar isso produz um runtime panic:
O runtime imprime a mensagem do panic, a goroutine que entrou em panic, o arquivo e a linha exatos, e encerra com um status code diferente de zero.
Você também pode lançar um panic explicitamente usando a função built-in panic, que aceita um valor do type any:
O argumento pode ser qualquer coisa — uma string, um error, um struct, um número. Quando panic é chamada, a função atual para imediatamente. Nenhum código após a chamada a panic é executado. O runtime começa a percorrer a call stack de volta.
Panic e defer
A chave para entender panic está na sua relação com defer. À medida que um panic percorre a call stack, ele executa todas as funções com defer em cada stack frame antes de seguir para o próximo. Funções com defer executam em ordem LIFO dentro de cada frame — a mesma ordem em que executariam em um return normal.
Após b entrar em panic, sua função com defer executa primeiro. O panic sobe para a, que executa sua função com defer. Por fim, chega a main, executa sua função com defer e então imprime a mensagem do panic e encerra.
Essa garantia — de que funções com defer sempre executam, mesmo durante um panic — é o que torna defer essencial para limpeza de recursos. File handles, conexões com banco de dados, mutexes e quaisquer outros recursos protegidos com defer são liberados mesmo que o programa entre em panic.
A função recover
recover é uma função built-in que interrompe a propagação de um panic e retorna o valor que foi passado para panic. Sua assinatura é:
Se chamada quando não há nenhum panic em andamento, recover retorna nil. Há um requisito estrito: recover deve ser chamada diretamente dentro de uma função com defer. Chamá-la em um helper que é chamado a partir de uma função com defer, ou em qualquer lugar fora de defer, não tem efeito.
O padrão canônico envolve recover em uma função anônima com defer:
Quando fn entra em panic, a função com defer executa, recover() captura o valor do panic, e safeRun retorna normalmente com um error definido. Quando fn não entra em panic, recover() retorna nil e a função com defer não faz nada.
O que acontece após o recover
Após recover capturar um panic, a goroutine em panic não retoma a função que entrou em panic. A função com defer contendo recover completa normalmente, e o controle retorna para quem chamou a função em panic. Tudo que viria após o panic na função original desaparece — nunca será executado.
riskyOp para no panic. main nunca chega ao seu Println final. O recover avança — de volta para quem chamou — não para o ponto onde o panic ocorreu.
Quando usar recover
O propósito de recover é restrito: evitar que um panic dentro de um subsistema encerre todo o processo. O caso de uso mais claro é um package que executa código sobre o qual não tem controle total — um sistema de plugins, um template engine, um request handler — onde um panic nesse código deve se tornar um error retornado em vez de derrubar o servidor.
Quem chama Execute recebe um error em vez de um crash. O package age como uma fronteira de segurança entre código imprevisível e o restante do programa.
Dois limites nessa abordagem merecem atenção.
Recovery não corrige o problema subjacente. Se a condição que causou o panic ainda existir — o mesmo nil pointer, o mesmo índice fora dos limites — e o mesmo código executar novamente, ele entrará em panic de novo. recover é para encerramento gracioso, não para repetir a operação que falhou.
Panic e recover não são exception handling. Usar panic para sinalizar falhas esperadas e recover para tratá-las — da forma como try-catch é usado em outras linguagens — produz código mais difícil de ler, de compor e que viola as expectativas de qualquer pessoa lendo Go idiomático. A regra é direta: retorne error para falhas esperadas, deixe panics propagarem para estados genuinamente irrecuperáveis, e use recover apenas em fronteiras de package para conter o dano e traduzi-lo em um error que quem chama pode tratar.