Materiales técnicos
Los programas en Go utilizan os.Exit
o log.Fatal*
para salir inmediatamente (usar panic
no es una buena forma de salir del programa, por favor no utilices panic).
Llama a os.Exit
o log.Fatal*
solo en main()
. Todas las demás funciones deben devolver errores al llamante.
No 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
}
En principio, los programas con múltiples puntos de salida tienen varios problemas:
- Flujo de control oscuro: cualquier función puede salir del programa, lo que dificulta razonar sobre el flujo de control.
- Difícil de probar: las funciones que abandonan el programa también abandonan las pruebas que las llaman. Esto hace que las funciones sean difíciles de probar e introduce el riesgo de omitir otras pruebas que aún no se han ejecutado por
go test
. - Omisión de limpieza: cuando una función abandona el programa, omite cualquier llamada de función diferida. Esto aumenta el riesgo de omitir tareas importantes de limpieza.
Salida única
Si es posible, solo debe haber un máximo de una llamada a os.Exit
o log.Fatal
en tu función main()
. Si hay múltiples escenarios de error que requieren detener la ejecución del programa, coloca esa lógica en una función separada y devuelve el error desde allí. Esto acortará la función main()
y pondrá toda la lógica empresarial crítica en una función separada y testeable.
Enfoque no recomendado:
package main
func main() {
args := os.Args[1:]
if len(args) != 1 {
log.Fatal("falta el archivo")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// Si llamamos a log.Fatal después de esta línea
// se ejecutará f.Close.
b, err := os.ReadAll(f)
if err != nil {
log.Fatal(err)
}
// ...
}
Enfoque recomendado:
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("falta el archivo")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
b, err := os.ReadAll(f)
if err != nil {
return err
}
// ...
}
El ejemplo anterior utiliza log.Fatal
, pero esta guía también se aplica a os.Exit
o a cualquier código de biblioteca que llame a os.Exit
.
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Puedes cambiar la firma de run()
según sea necesario. Por ejemplo, si tu programa debe salir con un código de fallo específico, run()
puede devolver el código de salida en lugar de un error. Esto también permite que las pruebas unitarias validen directamente este comportamiento.
func main() {
os.Exit(run(args))
}
func run() (exitCode int) {
// ...
}
Ten en cuenta que la función run()
utilizada en estos ejemplos no es obligatoria. El nombre, la firma y la configuración de la función run()
son flexibles. Entre otras cosas, puedes:
- Aceptar argumentos de línea de comandos no analizados (por ejemplo,
run(os.Args[1:])
) - Analizar los argumentos de la línea de comandos en
main()
y pasarlos arun
- Devolver el código de salida a
main()
con un tipo de error personalizado - Colocar la lógica empresarial en un nivel de abstracción diferente
package main
Esta guía solo requiere que haya un lugar en main()
responsable del flujo de salida real.