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