Технические материалы

Программы на языке 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() места, ответственного за фактический ход завершения выполнения программы.