Технические материалы
Программы на языке Go используют os.Exit или log.Fatal* для немедленного завершения выполнения (использование panic не является хорошим способом завершения программы, пожалуйста, не используйте panic).
Вызывайте os.Exit или log.Fatal* только внутри main(). Все остальные функции должны возвращать ошибки вызывающему.
Не рекомендуется:
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)
}
Рекомендуется:
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
}
В принципе, у программ с несколькими точками выхода есть несколько проблем:
- Неясный поток управления: любая функция может завершить программу, что затрудняет анализ потока управления.
- Трудности в тестировании: Функции, которые завершают программу, также завершают тесты, вызывающие их. Это делает эти функции сложными для тестирования и увеличивает риск пропуска других тестов, которые еще не запущены с помощью go test.
- Пропуск очистки: Когда функция завершает программу, она пропускает любые вызовы отложенных функций. Это увеличивает риск пропуска важных задач по очистке.
Одноразовый выход
При возможности в функции main() должен быть максимум один вызов os.Exit или log.Fatal. Если есть несколько сценариев ошибок, требующих остановки выполнения программы, поместите эту логику в отдельную функцию и верните ошибку оттуда. Это сократит код в main() и поместит всю критическую бизнес-логику в отдельную, тестируемую функцию.
Не рекомендуемый подход:
package main
func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("отсутствует файл")
  }
  name := args[0]
  f, err := os.Open(name)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()
  // Если мы вызовем log.Fatal после этой строки
  // f.Close будет выполнен.
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  // ...
}
Рекомендуемый подход:
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("отсутствует файл")
  }
  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
  }
  // ...
}
Приведенный выше пример использует log.Fatal, но эти рекомендации также применимы к os.Exit или любому библиотечному коду, который вызывает os.Exit.
func main() {
  if err := run(); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}
Вы можете изменить сигнатуру run() по мере необходимости. Например, если ваша программа должна завершиться с определенным кодом ошибки, run() может возвращать код завершения вместо ошибки. Это также позволяет модульным тестам непосредственно проверять это поведение.
func main() {
  os.Exit(run(args))
}
func run() (exitCode int) {
  // ...
}
Пожалуйста, обратите внимание, что функция run(), использованная в этих примерах, не является обязательной. Название, сигнатура и устройство функции run() гибкие. Среди прочего, вы можете:
- Принимать необработанные аргументы командной строки (например, run(os.Args[1:]))
- Разбирать аргументы командной строки в main()и передавать их вrun
- Возвращать код завершения в main()с помощью пользовательского типа ошибки
- Размещать бизнес-логику на другом уровне абстракции package main
Эти рекомендации требуют только наличия в main() места, ответственного за фактический ход завершения выполнения программы.