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 pararun
- 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.