Tutorial

Lidando com a função Panics em Go

GoDevelopment

Introdução

Os erros que um programa encontra se enquadram em duas grandes categorias: a dos erros que o programador previu e a dos que ele não previu. A interface de error - que abordamos nos dois artigos anteriores sobre Como lidar com erros - trata, em grande parte, dos erros esperados, à medida que escrevemos programas em Go. A interface de error nos permite reconhecer a rara possibilidade de um erro ocorrer a partir das chamadas de função, de modo que possamos responder adequadamente nessas situações.

Panics se enquadra na segunda categoria de erros, os quais não são previstos pelo programador. Esses erros imprevistos levam um programa a fechar espontaneamente e a sair do programa Go em execução. Com frequência, os erros comuns são os responsáveis pela criação de panics. Neste tutorial, examinaremos algumas maneiras como operações comuns podem produzir panics em Go, bem como veremos maneiras de evitar esses panics. Também usaremos as instruções defer junto com a função recover para captar panics, antes que tenham a chance de encerrar inesperadamente nossos programas do Go em execução.

Entendendo a função Panics

Existem certas operações em Go que retornam panics automaticamente e interrompem o programa. As operações comuns incluem indexar uma matriz para além de sua capacidade, executar as afirmações de tipo, chamar métodos em ponteiros nil, utilizar incorretamente exclusões mútuas e tentar trabalhar com canais fechados. A maioria destas situações resultam de erros cometidos durante a programação, em que o compilador não tem a capacidade de detectar enquanto compila o seu programa.

Uma vez que os panics incluem detalhes úteis para resolver um problema, normalmente, os desenvolvedores os utilizam como uma indicação de que cometeram um erro durante o desenvolvimento de um programa.

Panics fora dos limites

Ao tentar acessar um índice que vai além do tamanho de uma fatia ou da capacidade de uma matriz, o tempo de execução do Go irá gerar um panic.

O exemplo a seguir comete o erro comum de tentar acessar o último elemento de uma fatia usando o tamanho da fatia retornada pela função integrada len. Tente executar este código para ver por que isso pode produzir um panic:

package main

import (
    "fmt"
)

func main() {
    names := []string{
        "lobster",
        "sea urchin",
        "sea cucumber",
    }
    fmt.Println("My favorite sea creature is:", names[len(names)])
}

Isso terá o seguinte resultado:

Output
panic: runtime error: index out of range [3] with length 3 goroutine 1 [running]: main.main() /tmp/sandbox879828148/prog.go:13 +0x20

O nome do resultado do panic fornece uma dica: panic: runtime error: index out of range (pânico: erro de tempo de execução: índice fora do intervalo). Criamos uma fatia com três criaturas marinhas. Em seguida, tentamos obter o último elemento da fatia, indexando aquela fatia com o tamanho própria fatia - com a função integrada len. Lembre-se que fatias e matrizes são baseadas em zero; portanto, o primeiro elemento é zero e o último elemento nessa fatia está no índice 2. Considerando que tentamos acessar a fatia no terceiro índice, 3, não há nehum elemento na fatia para retornar porque ele está fora dos limites da fatia. Assim, ao tempo de execução não resta outra opção senão a de encerrar e sair, uma vez que pedimos a ele que fizesse algo impossível. Além disso, durante a compilação, o Go não tem como provar que esse código tentará fazer isso e, consequentemente, o compilador não consegue detectar a ocorrência.

Note também que o código subsequente não foi executado. Isso acontece porque um panic é um evento que impede completamente a execução do seu programa Go. A mensagem produzida contém vários fragmentos de informações úteis para diagnosticar a causa do panic.

Anatomia de um panic

Os panics são compostos por uma mensagem que indica a causa do panic e um rastreamento de pilha que ajuda a localizar onde, em seu código, o panic foi produzido.

A primeira parte de qualquer panic é a mensagem. Ela começará sempre com a string panic:, seguida de uma string que varia de acordo com a causa do panic. O panic do exercício anterior tem a mensagem:

panic: runtime error: index out of range [3] with length 3

A string runtime error: depois do prefixo panic: nos diz que o panic foi gerado pelo tempo de execução da linguagem. Esse panic nos diz que tentamos usar um índice [3] que estava fora do alcance do comprimento da fatia 3.

Após essa mensagem está o rastreamento de pilha. Os rastreamentos de pilha formam um mapa que podemos seguir para localizar exatamente qual linha de código estava executando quando o panic foi gerado e como aquele código foi invocado pelo código anterior.

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20

O rastreamento de pilha do exemplo anterior mostra que nosso programa gerou o panic a partir do arquivo /tmp/sandbox879828148/prog.go,​​​ na linha de número 13. Além disso, ele nos diz que esse panic foi gerado na função main() do pacote main.

O rastreamento de pilha é dividido em blocos separados — sendo um para cada goroutine de seu programa. Toda execução do programa Go é realizada por uma ou mais goroutines que podem, cada qual - de modo simultâneo e independente, executar partes de seu código Go. Cada block inicia com o cabeçalho goroutine X [state]: O cabeçalho dá o número da ID da goroutine junto com o estado em que ela estava quando o panic ocorreu. Após o cabeçalho, o rastreamento de pilha mostra a função que o programa estava executando quando o panic aconteceu, junto com o nome do arquivo e número da linha onde a função foi executada.

O panic no exemplo anterior foi gerado por um acesso fora dos limites de uma fatia. Os panics também podem ser gerados quando os métodos são chamados nos ponteiros que não foram definidos.

Receptores nulos

A linguagem de programação Go tem ponteiros para se referir à instância específica de algum tipo existente na memória do computador em tempo de execução. Os ponteiros podem assumir o valor nil, o que indica que não estão apontando para nada. Quando tentarmos chamar métodos em um ponteiro que é nil, o tempo de execução do Go irá gerar um panic. De igual modo, as variáveis que são tipos de interface também produzirão panics quando os métodos forem chamados neles. Para ver os panics gerados nesses casos, teste o seguinte exemplo:

package main

import (
    "fmt"
)

type Shark struct {
    Name string
}

func (s *Shark) SayHello() {
    fmt.Println("Hi! My name is", s.Name)
}

func main() {
    s := &Shark{"Sammy"}
    s = nil
    s.SayHello()
}

Os panics produzidos se parecerão com isto:

Output
panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba] goroutine 1 [running]: main.(*Shark).SayHello(...) /tmp/sandbox160713813/prog.go:12 main.main() /tmp/sandbox160713813/prog.go:18 +0x1a

Neste exemplo, definimos um struct chamado Shark. O Shark tem um método definido em seu ponteiro receptor chamado SayHello, que irá imprimir uma saudação para a saída padrão quando chamado. Dentro do corpo da nossa função main, criamos uma nova instância desse struct Shark e pedimos um ponteiro a ela, usando o operador &. Esse ponteiro está atribuído à variável s. Em seguida, vamos reatribuir a variável s para o valor nil com a instrução s = nil. Por fim, tentamos chamar o método SayHello na variável s. Em vez de receber uma mensagem amigável do Sammy, recebemos um panic de que tentamos acessar um endereço de memória inválido. Como a variável s é nil, quando a função SayHello é chamada, ela tenta acessar o campo Name no tipo *Shark. Como se trata de um ponteiro receptor e, neste caso, de um receptor que é nil, ele entra em pânico, uma vez que não consegue desreferenciar um ponteiro nil.

A despeito de termos definido s para nil de maneira explícita neste exemplo, na prática isso ocorre não ocorre de modo tão claro. Ao ver panics envolvendo nil pointer dereference, certifique-se de ter atribuído devidamente quaisquer variáveis de ponteiros que possa ter criado.

Panics gerados a partir de ponteiros nulos e de acessos fora dos limites são dois panics de ocorrência comum, sendo gerados pelo tempo de execução. Também é possível gerar um panic manualmente, usando uma função integrada.

Usando o função integrada panic

Também podemos gerar nossos próprios panics usando a função integrada panic. Essa função utiliza uma string única como argumento, que é a mensagem que o panic produzirá. Normalmente, essa mensagem resulta menos prolixa do que reescrever o código para retornar um erro. Além disso, podemos usar isso dentro dos nossos próprios pacotes para indicar aos desenvolvedores que eles podem ter cometido um erro ao usar o código do nosso pacote. Sempre que possível, o melhor a se fazer é tentar retornar valores de error para os consumidores de nossos pacotes.

Execute este código para ver um panic gerado a partir de uma função chamada de outra função:

package main

func main() {
    foo()
}

func foo() {
    panic("oh no!")
}

O resultado do panic produzido fica parecido com este:

Output
panic: oh no! goroutine 1 [running]: main.foo(...) /tmp/sandbox494710869/prog.go:8 main.main() /tmp/sandbox494710869/prog.go:4 +0x40

Aqui, definimos uma função foo que chama o panic integrado com a string "oh no!". Essa função é chamada por nossa função main. Observe que o resultado tem a mensagem panic: oh no! e o rastreamento de pilha mostra uma goroutine única com duas linhas no rastreamento de pilha: uma para a função main() e outra para nossa função foo().

Vimos que os panics parecem encerrar nosso programa no local em que foram gerados. Isso pode criar problemas quando houver recursos abertos que precisam ser fechados de maneira adequada. O Go fornece um mecanismo para sempre executar alguns códigos, mesmo na presença de um panic.

Funções adiadas

Seu programa pode ter recursos que ele deve limpar de maneira adequada, mesmo enquanto um panic estiver sendo processado em tempo de execução. O Go permite que você adie a execução de uma chamada de função até que sua chamada tenha concluído a execução. As funções adiadas são executadas mesmo na presença de um panic. Elas são usadas como um mecanismo de segurança para proteger o programa da natureza caótica dos panics. As funções são adiadas quando as chamamos como de costume e depois fazemos a prefixação da instrução toda, usando a palavra-chave defer, como em defer sayHello(). Execute este exemplo para ver como uma mensagem pode ser impressa, ainda que um panic tenha sido produzido:

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("hello from the deferred function!")
    }()

    panic("oh no!")
}

O resultado produzido a partir desse exemplo se parecerá com:

Output
hello from the deferred function! panic: oh no! goroutine 1 [running]: main.main() /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

Dentro da função main desse exemplo, fazemos primeiro o defer (adiamos) de uma chamada para uma função anônima que imprime a mensagem "hello from the deferred function!". Na sequência, a função main produz imediatamente um panic usando a função panic. No resultado desse programa, vemos primeiro que a função adiada é executada e imprime sua mensagem. Depois disso, vemos o panic que geramos na função main.

As funções adiadas proporcionam proteção contra a natureza surpreendente dos panics. Dentro das funções adiadas, o linguagem de programação Go também nos dá a oportunidade de impedir um panic de encerrar nosso programa Go, usando outra função integrada.

Lidando com os panics

Os panics possuem um mecanismo de recuperação único — a função integrada recover. Essa função permite que você intercepte um panic em seu caminho até a pilha de chamadas e impeça-o de encerrar inesperadamente o seu programa. Ela possui regras estritas de uso, mas pode ser algo inestimável em um aplicativo de produção.

Como ela faz parte do pacote builtin, a função recover pode ser chamada sem importar nenhum pacote adicional:

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    return a / b
}

Este exemplo produzirá o seguinte:

Output
2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero we survived dividing by zero!

Nesse exemplo, nossa função main chama uma função que definimos, a divideByZero. Dentro dessa função, usamos a função defer para adiar uma chamada para uma função anônima - responsável por lidar com quaisquer panics que possam surgir durante o execução da função divideByZero. Dentro dessa função anônima adiada, chamamos a função integrada recover (recuperar) e atribuímos o erro que ela retorna para uma variável. Se o divideByZero entrar em pânico, esse valor de error será definido, caso contrário, ele será nil (nulo ou inválido). Ao comparar a variável err com a nil, podemos detectar se um panic ocorreu, e neste caso, registramos o evento de panic usando a função log.Println - como se fosse qualquer outro error.

Depois dessa função anônima adiada, chamados outra função que definimos - a divide - e tentamos imprimir seus resultados usando fmt.Println. Os argumentos fornecidos farão a divide executar uma divisão por zero, o que produzirá um panic.

No resultado para esse exemplo, vemos primeiro a mensagem de registro da função anônima que recupera o panic, seguida da mensagem we survived dividing by zero! (sobrevivemos à divisão por zero!). Nós fizemos isso, graças à função integrada recover que impediu que um panic catastrófico fechasse nosso programa em Go.

O valor de err retornado do recover() é exatamente o valor que foi fornecido à chamada para o panic(). Assim, é crucial garantir que o valor de err somente seja nil na ausência de um panic.

Detectando panics com o recover

A função recover depende do valor do erro para fazer determinações quanto a se um panic ocorreu ou não. Como o argumento para a função panic é uma interface vazia, ele pode ser de qualquer tipo. O valor zero para qualquer tipo de interface, incluindo a interface vazia, é nil. Tome cuidado para evitar o nil como um argumento para panic, como mostrado por este exemplo:

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    if b == 0 {
        panic(nil)
    }
    return a / b
}

Isso resultará em:

Output
we survived dividing by zero!

Esse exemplo é o mesmo que o exemplo anterior, que envolve o recover com algumas pequenas alterações. A função divide foi alterada para verificar se seu divisor b é igual a 0. Caso seja, ele irá gerar um panic usando a função panic integrada com um argumento de nil. O resultado, dessa vez, não inclui a mensagem de registro mostrando que um panic ocorreu mesmo que um tenha sido criado pelo divide. Esse comportamento silencioso acontece porque é muito importante garantir que o argumento para a função integrada panic não seja nil.

Conclusão

Vimos várias maneiras para a criação de panics em Go e como eles podem ser recuperados usando a função recover integrada. Ainda que você não faça, necessariamente, uso da função de panic, propriamente dita, a recuperação adequada dos eventos de panics é um passo importante para deixar os aplicativos em Go prontos para a produção.

Você também pode explorar nossa série de artigos sobre Como codificar em Go.

Creative Commons License