Materiais técnicos

Os programas em Go utilizam os.Exit ou log.Fatal* para sair imediatamente (usar panic não é uma boa forma de encerrar o programa, por favor, não use panic).

Chame somente um de os.Exit ou log.Fatal* dentro de main(). Todas as outras funções devem retornar erros para o chamador.

Não recomendado:

func main() {
  body := readFile(path)
  fmt.Println(body)
}
func readFile(path string) string {
  f, err := os.Open(path)
  if err != nil {
    log.Fatal(err)
  }
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  return string(b)
}

Recomendado:

func main() {
  body, err := readFile(path)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(body)
}
func readFile(path string) (string, error) {
  f, err := os.Open(path)
  if err != nil {
    return "", err
  }
  b, err := os.ReadAll(f)
  if err != nil {
    return "", err
  }
  return string(b), nil
}

Em princípio, programas com vários pontos de saída têm diversos problemas:

  • Fluxo de controle obscuro: Qualquer função pode encerrar o programa, tornando difícil a compreensão do fluxo de controle.
  • Difícil de testar: Funções que encerram o programa também encerram os testes que as chamam. Isso torna as funções difíceis de testar e introduz o risco de pular outros testes ainda não executados pelo go test.
  • Omissão de limpeza: Quando uma função encerra o programa, ela ignora chamadas de funções adiadas. Isso aumenta o risco de ignorar tarefas importantes de limpeza.

Saída única

Se possível, deve haver um máximo de uma chamada para os.Exit ou log.Fatal em sua função main(). Se houver vários cenários de erro que exigem a interrupção da execução do programa, coloque essa lógica em uma função separada e retorne o erro de lá. Isso irá encurtar a função main() e colocar toda a lógica de negócios crítica em uma função separada e testável.

Abordagem não recomendada:

package main
func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("arquivo ausente")
  }
  nome := args[0]
  f, err := os.Open(nome)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()
  // Se chamarmos log.Fatal após esta linha
  // f.Close será executado.
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  // ...
}

Abordagem recomendada:

package main
func main() {
  if err := run(); err != nil {
    log.Fatal(err)
  }
}
func run() error {
  args := os.Args[1:]
  if len(args) != 1 {
    return errors.New("arquivo ausente")
  }
  nome := args[0]
  f, err := os.Open(nome)
  if err != nil {
    return err
  }
  defer f.Close()
  b, err := os.ReadAll(f)
  if err != nil {
    return err
  }
  // ...
}

O exemplo acima usa log.Fatal, mas essa diretriz também se aplica a os.Exit ou a qualquer código de biblioteca que chame os.Exit.

func main() {
  if err := run(); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

Você pode alterar a assinatura de run() conforme necessário. Por exemplo, se o seu programa precisar encerrar com um código de falha específico, run() pode retornar o código de saída em vez de um erro. Isso também permite que os testes de unidade validem diretamente esse comportamento.

func main() {
  os.Exit(run(args))
}

func run() (exitCode int) {
  // ...
}

Observe que a função run() usada nestes exemplos não é obrigatória. O nome, a assinatura e a configuração da função run() são flexíveis. Entre outras coisas, você pode:

  • Aceitar argumentos de linha de comando não analisados (por exemplo, run(os.Args[1:]))
  • Analisar argumentos de linha de comando em main() e passá-los para run
  • Retornar o código de saída para main() com um tipo de erro personalizado
  • Colocar a lógica de negócios em um nível de abstração diferente de package main

Essa diretriz apenas exige que haja um local em main() responsável pelo fluxo de saída real.