A quinta

A quinta

Exercício de aprendizagem

Introdução

A interface de erro

O tratamento de erros não é feito com exceções em Go. Em vez disso, os erros são valores normais de tipos que implementam a interface error incorporada. A interface error é muito reduzida. Contém apenas um método, Error(), que devolve a mensagem de erro como uma string.

type error interface {
  Error() string
}

Sempre que defines uma função na qual pode ocorrer um erro durante a execução que tenha de chegar a quem a chama, tens de incluir error como um dos tipos de retorno. Se a função tiver vários valores devolvidos, por convenção error é sempre o último.

func DoSomething() (int, error) {
  // ...
}

Criar e devolver um erro simples

Nem sempre tens de implementar a interface de erro tu próprio. Para criar um erro simples, podes usar a função errors.New(), que faz parte do pacote errors da biblioteca padrão. A única coisa que precisas de passar é a mensagem de erro como uma string, e errors.New() encarrega-se de criar um valor que contém a tua mensagem e implementa a interface error.

Se a função devolver um erro, é boa prática devolver o valor zero para todos os outros valores devolvidos:

func DoSomething() (SomeStruct, int, error) {
  // ...
  return SomeStruct{}, 0, errors.New("failed to calculate result")
}
Caution

Não deves assumir que todas as funções devolvem valores zero para os outros valores devolvidos se houver um erro. É boa prática assumir que não é seguro usar nenhum dos outros valores devolvidos se ocorreu um erro. As únicas exceções são os casos em que a documentação afirma claramente que os outros valores devolvidos são relevantes em caso de erro.

Se quiseres usar um erro simples destes em vários sítios, deves declarar uma variável para o erro em vez de usares errors.New em linha. Por convenção, o nome da variável deve começar por Err ou err (consoante seja exportada ou não). Estas variáveis de erro são muitas vezes chamadas erros sentinela.

import "errors"

var ErrNotFound = errors.New("resource was not found")

func DoSomething() error {
  // ...
  return ErrNotFound
}

Devolve nil no erro para sinalizar que não houve erros durante a execução da função:

func Foo() (int, error) {
  return 10, nil
}

Verificação de erros

Se chamares uma função que devolve um erro, é comum guardar o valor do erro numa variável chamada err. Antes de usares o resultado propriamente dito da função, tens de verificar que não houve erro.

Para evitares aninhar o "caminho feliz" do teu código, os casos de erro devem ser tratados primeiro. Podemos usar == e != para comparar o erro com nil e sabemos que houve um erro quando err não é nil.

func processUserFile() error {
	file, err := os.Open("./users.csv")
	if err != nil {
		return err
	}

	// do something with file
}

Na maioria das vezes, o erro é devolvido pela pilha de funções para cima, tal como mostra o exemplo acima. Outra forma de tratar o erro pode ser registá-lo e continuar com outra operação. É boa prática devolver o erro ou registá-lo, nunca as duas coisas.

Como a maioria das funções em Go inclui um erro como um dos valores devolvidos, vais ver e usar o padrão if err != nil por todo o lado no código Go.

Tipos de erro personalizados

Se quiseres que o teu erro inclua mais informação do que apenas a string da mensagem de erro, podes criar um tipo de erro personalizado. Como já foi referido, tudo o que implemente a interface error (ou seja, que tenha um método Error() string) pode servir de erro em Go.

Normalmente, usa-se uma struct para criar um tipo de erro personalizado. Por convenção, os nomes dos tipos de erro personalizados devem terminar em Error. Além disso, é melhor definir o método Error() string com um recetor ponteiro; vê este comentário do Stackoverflow para perceberes o raciocínio. Repara que isto significa que tens de devolver um ponteiro para o teu erro personalizado; caso contrário, não conta como error, porque o valor não-ponteiro não fornece o método Error() string.

type MyCustomError struct {
  message string
  details string
}

func (e *MyCustomError) Error() string {
  return fmt.Sprintf("%s, details: %s", e.message, e.details)
}

func someFunction() error {
  // ...
  return &MyCustomError{
    message: "...",
    details: "...",
  }
}

Instruções

Chegou finalmente o dia que esperaste durante tanto tempo: és agora o orgulhoso proprietário de uma bela quinta nos Alpes.

Continuas a não gostar de acordar demasiado cedo de manhã para alimentar as tuas vacas. Como és um excelente engenheiro, construíste um dispensador de comida, o FEED-M-ALL.

A última coisa de que precisas para terminar o teu projeto é um pedaço de código que calcule a quantidade de forragem que cada vaca deve receber. É importante que cada vaca receba a mesma quantidade; tens de evitar conflitos. As vacas são muito sensíveis.

Felizmente, não precisas de descobrir sozinho todas as fórmulas para calcular as quantidades de forragem. Usas uma misteriosa biblioteca externa que encontraste na internet. Supostamente, deve resultar nas vacas mais felizes. A biblioteca expõe um tipo que cumpre a seguinte interface. Vais apoiar-te nisto no código que escreveres.

type FodderCalculator interface {
	FodderAmount(int) (float64, error)
	FatteningFactor() (float64, error)
}

À medida que trabalhas no teu código, vais melhorar o tratamento de erros para o tornar mais robusto e mais fácil de depurar mais tarde, quando o usares no teu dia a dia na quinta.

1. Divide a comida de forma igual

Antes de mais, concentras-te em escrever o código necessário para calcular a quantidade de forragem por vaca.

Implementa uma função DivideFood que aceita, como argumentos, um FodderCalculator e um número de vacas (um número inteiro). Para esta tarefa, assumes que o número de vacas recebido é sempre maior do que zero. A função deve devolver a quantidade de comida por vaca como float64 ou um erro, caso tenha ocorrido algum.

Para fazer o cálculo, precisas primeiro de obter a quantidade total de forragem para todas as vacas. Fazes isso chamando o método FodderAmount e passando-lhe o número de vacas. Além disso, precisas de um fator pelo qual essa quantidade tem de ser multiplicada. Obténs esse fator chamando o método FatteningFactor. Com estes dois valores e o número de vacas, podes agora calcular a quantidade de comida por vaca (como float64). É isso que a função DivideFood deve devolver.

Se um dos métodos que chamas devolver um erro, a execução deve parar e esse erro deve ser devolvido (tal como está) pela função DivideFood.

// For this example, we assume FodderAmount returns 50
// and FatteningFactor returns 1.5.
DivideFood(fodderCalculator, 5)
// => 15 <nil>

// Now assuming FodderAmount returns an error with message "something went wrong".
DivideFood(fodderCalculator, 5)
// => 0 "something went wrong"

2. Verifica o número de vacas

Enquanto trabalhavas na primeira tarefa acima, percebeste que a biblioteca externa que usas não tem tanta qualidade como pensavas. Por exemplo, não consegue lidar corretamente com valores de entrada inválidos. Queres contornar esta limitação acrescentando uma verificação do valor de entrada no teu próprio código.

Escreve uma função ValidateInputAndDivideFood com a mesma assinatura que a DivideFood acima.

  • Se o número de vacas recebido for maior do que 0, a função deve chamar DivideFood e devolver o resultado dessa chamada.
  • Se o número de vacas for 0 ou menos, a função deve devolver um erro com a mensagem "invalid number of cows".
ValidateInputAndDivideFood(fodderCalculator, 5)
// => 15 <nil>

ValidateInputAndDivideFood(fodderCalculator, -2)
// => 0 "invalid number of cows"

3. Melhora o tratamento de erros

Verificar o número de vacas antes de o passar adiante foi uma boa ideia, mas não estás totalmente satisfeito com a mensagem de erro pouco específica. Decides fazer melhor, criando um tipo de erro personalizado chamado InvalidCowsError.

O erro personalizado deve conter o número de vacas (int) e uma mensagem personalizada (string), e o método Error deve serializar os dados no seguinte formato:

{number of cows} cows are invalid: {custom message}

Agora que tens o teu erro personalizado, implementa uma função ValidateNumberOfCows que aceita o número de vacas como número inteiro e devolve um erro (ou nil).

  • Se o número de vacas for menor do que 0, a função devolve um InvalidCowsError com a mensagem personalizada definida como "there are no negative cows".
  • Se o número de vacas for 0, a função devolve um InvalidCowsError com a mensagem personalizada definida como "no cows don't need food".
  • Caso contrário, a função devolve nil para indicar que a validação foi bem-sucedida.
err := ValidateNumberOfCows(-5)
err.Error()
// => "-5 cows are invalid: there are no negative cows"

Depois do trabalho árduo de montar esta função de validação, reparas que já anoiteceu e deixas a tua secretária para apreciar o pôr do sol sobre as montanhas. Deixas para outro dia a tarefa de adicionar efetivamente a nova função de validação ao teu código.

Editar via GitHub A ligação abre numa nova janela ou separador
Go Exercism

Estás pronto para começar A quinta?

Inscreve-te no Exercism para aprenderes e dominares Go com 34 conceitos166 exercícios, e mentoria humana real, tudo grátis.