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