Materiali tecnici

I programmi Go utilizzano os.Exit o log.Fatal* per uscire immediatamente (utilizzare panic non è un buon modo per terminare il programma, si prega di non utilizzare panic).

Chiamare solo uno tra os.Exit o log.Fatal* in main(). Tutte le altre funzioni devono restituire errori al chiamante.

Non raccomandato:

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

Consigliato:

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

In principio, i programmi con punti di uscita multipli presentano diversi problemi:

  • Flusso di controllo oscuro: qualsiasi funzione può terminare il programma, rendendo difficile ragionare sul flusso di controllo.
  • Difficile da testare: le funzioni che terminano il programma terminano anche i test che le chiamano. Ciò rende difficile testare le funzioni e introduce il rischio di saltare altri test non ancora eseguiti da go test.
  • Saltare la pulizia: quando una funzione termina il programma, salta qualsiasi chiamata di funzione differita. Questo aumenta il rischio di saltare compiti importanti di pulizia.

Uscita singola

Se possibile, ci dovrebbe essere al massimo una chiamata a os.Exit o log.Fatal nella tua funzione main(). Se ci sono più scenari di errore che richiedono di interrompere l'esecuzione del programma, inserisci quella logica in una funzione separata e restituisci l'errore da lì. Questo accorcerà la funzione main() e metterà tutta la logica aziendale critica in una funzione separata e testabile.

Approccio non raccomandato:

package main
func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("file mancante")
  }
  nome := args[0]
  f, err := os.Open(nome)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()
  // Se chiamiamo log.Fatal dopo questa riga
  // f.Close verrà eseguito.
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  // ...
}

Approccio consigliato:

package main
func main() {
  if err := esegui(); err != nil {
    log.Fatal(err)
  }
}
func esegui() error {
  args := os.Args[1:]
  if len(args) != 1 {
    return errors.New("file mancante")
  }
  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
  }
  // ...
}

L'esempio sopra utilizza log.Fatal, ma questa linea guida si applica anche a os.Exit o a qualsiasi codice di libreria che chiama os.Exit.

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

È possibile modificare la firma di esegui() come necessario. Ad esempio, se il programma deve terminare con un codice di errore specifico, esegui() può restituire il codice di uscita anziché un errore. Ciò consente anche ai test unitari di convalidare direttamente questo comportamento.

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

func esegui() (codiceUscita int) {
  // ...
}

Si noti che la funzione esegui() utilizzata in questi esempi non è obbligatoria. Il nome, la firma e la configurazione della funzione esegui() sono flessibili. Tra le altre cose, è possibile:

  • Accettare argomenti non analizzati dalla riga di comando (ad esempio, esegui(os.Args[1:]))
  • Analizzare gli argomenti della riga di comando in main() e passarli a esegui
  • Restituire il codice di uscita a main() con un tipo di errore personalizzato
  • Mettere la logica aziendale a un diverso livello di astrazione in package main

Questa linea guida richiede solo che ci sia un punto in main() responsabile del flusso di uscita effettivo.