Desenvolvimento

3 abr, 2017

Dicas de como utilizar Go defer

Publicidade

Usar defer e poder adiar um comando para ser executado apenas quando a função terminar não importando a forma como ela terminar é uma grande ideia. Imagine uma função com vários pontos de retorno, seja para retornar erros ou porque ela terminou corretamente; ficar gerenciando fechar arquivos, conexões, mutex e o que mais você estiver usando em cada retorno é entediante e inseguro. Além do que, você pode esquecer um dos retornos, e pode apostar que esse único retorno que você esqueceu é que vai tirar horas do seu sono.

Mas defer não vem de graça. Como o sistema vai empilhar o comando que você mandou, adiar uma chamada usando defer será alguns microsegundos mais lento do que uma chamada direta. Nada para se preocupar no dia a dia, mas se você precisa extrair cada grama de performance do seu código, pode ser um ponto para observar… (ou talvez escrever em C haha)

O uso mais comum do defer é para liberar recursos, como fechar um arquivo, mas qualquer comando pode ser “adiado”. Por exemplo, eu tenho uma função complexa que faz muitas coisas e não pode rodar em paralelo no sistema, então, eu usei um mutex para sincronizar o código e garantir que não haveria colisão, mas como a função tem muitos pontos de retorno, o defer me ajudou a garantir que o lock seja liberado.

m := &sync.Mutex{}
.
.
.
m.Lock()
defer m.Unlock()

Fique atento

Outra coisa para se ter em mente é que você precisa tomar cuidado para não sobrepor retornos nomeados com defer, por exemplo, vamos dizer que você abriu um arquivo e vai usar defer para garantir que o arquivo seja fechado.

f, err = os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

Só que o gometalinter marcou com um warning a linha do defer porque a função Close() retorna um erro e você não esta tratando esse erro. Você considera isso inaceitável porque você segue minha cartilha que diz que warnings são erros! Então, você cria uma função anônima para ser chamada pelo defer que vai fechar o arquivo e tratar esse erro.

defer func() {
	err = f.Close()
	if err != nil {
		println(err.Error())
	} 
}()

A primeira vista esta tudo bem, você tratou o erro, o warning sumiu e você voltou a ter paz de espirito. O único problema é que a variável err é um retorno nomeado da sua função e como a função anônima vai ser chamada quando a rotina principal chegar ao fim, err vai ser sobrescrito pelo retorno de Close() e você nunca vai conseguir pegar o erro real da função; e vai passar horas se perguntando por que não recebe o resultado esperado e ao mesmo tempo nenhum erro.

Existem muitas formas de resolver esse problema, por exemplo, podemos usar outra variável no lugar de err, ou podemos simplesmente ignorar esse erro, já que é improvável que ele seja relevante; ou ainda, podemos ser radical e trocar o log por um panic. E também é bom lembrar que esse não é um problema exclusivo do defer. Teoricamente, você pode ter o mesmo problema com goroutines e até acabar com usa situação de data race.

Apesar desses detalhes que temos que observar, no geral é uma boa prática usar defer e garantir que os recursos alocados sejam liberados, por exemplo, recentemente no projeto pREST, um belo memory leak foi resolvido simplesmente usando defer para garantir que a conexão com o banco seria fechada.