Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 14 из 25 ~16 мин +50 XP

Ошибки и коды выхода: SilenceUsage

Cobra обработку ошибок сводит к короткому контракту: RunE возвращает error, Execute отдаёт его в main, и от того, что main делает дальше, зависит код выхода процесса. Разберём SilenceUsage и SilenceErrors, словарь кодов выхода для скриптов и CI, разделение stdout и stderr и оборачивание ошибок через %w.

Содержание урока · 10 разделов

Пайплайн зелёный, а бэкапа нет — классический сюжет с CLI-утилитой, которая упала, но вернула процессу ноль. Ноль для CI означает «всё хорошо»: едут следующие шаги, ошибка всплывает через неделю, когда восстановление из несуществующего бэкапа срочно нужно продакшену. В Cobra ответственность за код выхода лежит на двух концах. Первый — RunE возвращает error вместо того, чтобы вызывать os.Exit прямо из команды. Второй — main обрабатывает то, что вернул Execute, и превращает ошибку в ненулевой код. Пропустите второй конец — и любая ошибка превратится в вежливый ноль, потому что Execute ничего не завершает сама. Разберём всю линию целиком: от ошибки в RunE через печать в stderr до кода выхода, который читает шелл, и по пути уберём из вывода шум в виде usage-страницы, которая по умолчанию вываливается после каждой ошибки.

Cobra обработка ошибок: RunE вместо os.Exit#

Контракт простой: RunE возвращает error, Cobra печатает его и usage-страницу, Execute отдаёт ошибку вызывающему. Звать os.Exit внутри RunE не нужно и вредно: os.Exit завершает процесс немедленно, минуя defer, а значит, открытые в команде файлы не закроются и PostRun-хуки из предыдущего урока не выполнятся. Пока ошибка возвращается по цепочке, у программы остаётся шанс прибраться. Стандартный хвост main выглядит так: if err := rootCmd.Execute(); err != nil { os.Exit(1) } — сообщение об ошибке уже напечатано Cobra, main лишь выставляет код. Забытый хвост — самый дешёвый баг года: Execute вернул ошибку, main дошёл до конца нормально, процесс закрылся с нулём, и мониторинг остался в неведении. Проверьте свой main прямо сейчас, это одна строка, — и переходите к тонкостям печати.

main, который не прячет падение

GO
func main() {
        if err := rootCmd.Execute(); err != nil {
                os.Exit(1)
        }
}

Execute печатает Error: с текстом ошибки в stderr и возвращает ошибку; main превращает её в код выхода 1. Этого достаточно для скриптов и CI: ненулевой код — сигнал «упало». Более тонкие коды и собственная печать добавляются поверх этой схемы, а не вместо неё.

Проверка: код выхода у забывчивого main

main выглядит так: rootCmd.Execute() без обработки результата. RunE вернул ошибку. Какой код выхода у процесса?

SilenceUsage и SilenceErrors: тише — лучше#

По умолчанию ошибка в RunE тянет за собой usage-страницу: пользователь опечатался во флаге и получил два экрана списка команд, среди которых ещё нужно найти саму ошибку. Для человека это шум, для скрипта — мусор в stderr. rootCmd.SilenceUsage = true убирает печать usage при ошибках; справка по --help продолжает работать, потому что SilenceUsage глушит только реакцию на ошибку, а не сам механизм usage. SilenceErrors идёт дальше: Cobra не печатает и само сообщение Error:, ошибка возвращается нетронутой, и печать берёт на себя main — один формат, одно место. Флаги можно ставить и на конкретную команду, а проверка в ExecuteC смотрит оба уровня: достаточно SilenceUsage или SilenceErrors на команде или на корне, чтобы соответствующая печать заглохла. Рабочий дефолт для утилит с командами — SilenceUsage на корне: пользователи знают, что флаги перечислены в --help, и не нуждаются в их повторе после каждой опечатки.

SilenceUsage на корне

GO
var rootCmd = &cobra.Command{
        Use:          "notes",
        SilenceUsage: true,
}

func main() {
        if err := rootCmd.Execute(); err != nil {
                os.Exit(1) // Error: уже напечатан Cobra в stderr
        }
}

SilenceUsage глушит печать usage при ошибке: пользователь видит одну строку Error: с текстом вместо страницы команд. Само сообщение Cobra печатает сама, поэтому main только выставляет код выхода. Хотите свой формат ошибки — добавьте SilenceErrors и печатайте в main сами, но выберите одно место печати, а не оба.

Что напечатает Execute

Прочитайте код и выберите, что он выведет:

var rootCmd = &cobra.Command{
        Use:           "notes",
        SilenceUsage:  true,
        SilenceErrors: true,
}
// RunE возвращает errors.New("boom")

_ = rootCmd.Execute()

Коды выхода: язык для скриптов и CI#

Код выхода — единственный канал, через который утилита говорит с обёртками: шелл читает $?, CI сравнивает его с нулём, оркестратор решает, перезапускать ли контейнер. База простая: ноль — успех, всё остальное — ошибка. Но у разветвлённых утилит полезен словарь кодов: 1 — общий провал, 2 — ошибка использования, как поступает ряд системных утилит, 3 — частичный успех вроде «синхронизировано 9 из 12». Словарь фиксируется в документации рядом с описанием команд, а не в голове автора, — иначе скриптописатели начнут угадывать. Реализация аккуратно укладывается в тип: RunE возвращает свой тип ошибки с полем Code, main разбирает его через errors.As и зовёт os.Exit с нужным числом. Дополнение к словарю: код 128+N шелл показывает, когда процесс убит сигналом N — знакомый всем 130 после Ctrl+C это 128+2. Возвращайте такой код и сами, когда прерывание пришло извне, а cleanup успел завершиться: скрипты отличат отмену от обычного провала. И помните свойство os.Exit из начала урока: он завершает процесс немедленно и defer не выполняет, поэтому весь словарь живёт в main, а не в командах.

Тип ошибки с кодом выхода

GO
type ExitError struct {
        Code int
        Err  error
}

func (e *ExitError) Error() string { return e.Err.Error() }
func (e *ExitError) Unwrap() error { return e.Err }

func main() {
        err := rootCmd.Execute()
        if err == nil {
                return
        }
        var ee *ExitError
        if errors.As(err, &ee) {
                fmt.Fprintln(os.Stderr, err)
                os.Exit(ee.Code)
        }
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
}

ExitError несёт код поверх обычной ошибки и разворачивается через Unwrap, поэтому errors.Is и errors.As продолжают видеть исходную причину. RunE возвращает &ExitError{Code: 3, Err: ...} там, где сценарий «частичный успех», а main один раз разбирает тип и выставляет код. Печать в обеих ветках идёт в os.Stderr: stdout остаётся чистым для перенаправлений — правило разделения потоков работает и здесь. Словарь кодов живёт комментарием рядом с этим типом.

Проверка: os.Exit и defer

RunE открыл файл и зарегистрировал закрытие через defer f.Close(). Внутри RunE вызван os.Exit(3). Что произойдёт?

Оборачивание ошибок: %w и контекст#

Ошибка «permission denied» без контекста — детектив для пользователя: что именно не открылось и в какой команде? fmt.Errorf("read %s: %w", path, err) добавляет путь и причину в одну строку, а глагол %w сохраняет исходную ошибку внутри: errors.Is(err, os.ErrNotExist) и errors.As продолжают работать по всей обёрнутой цепочке. В CLI это превращается в конкретику двух уровней: RunE добавляет операцию — что делали, main добавляет команду — где делали. Разверните привычку из языка Go: каждый слой добавляет контекст, ничего не теряя, и ваши сообщения станут самодиагностируемыми — по одной строке из лога видно, какая команда, на каком файле и по какой причине упала. Полная механика %w, errors.Is и errors.As разобрана отдельным уроком в продвинутом курсе, здесь мы используем только её следствия.

Обёртка и разбор в main

GO
func runAdd(cmd *cobra.Command, args []string) error {
        data, err := os.ReadFile(storagePath)
        if err != nil {
                return fmt.Errorf("read storage: %w", err)
        }
        // ... работа с заметками
        return nil
}

func main() {
        if err := rootCmd.Execute(); err != nil {
                if errors.Is(err, os.ErrNotExist) {
                        fmt.Fprintln(os.Stderr, "хранилище не найдено, выполните notes init")
                        os.Exit(2)
                }
                fmt.Fprintln(os.Stderr, err)
                os.Exit(1)
        }
}

RunE оборачивает ошибку чтения через %w, сохраняя os.ErrNotExist внутри цепочки. main разворачивает её через errors.Is и переводит в подсказку с отдельным кодом 2: скрипт по коду отличит «нет хранилища» от общего провала и запустит init. Это словарь кодов в действии — без единого os.Exit внутри команды.

stderr против stdout: кто что читает#

Разделение потоков — половина культуры вывода CLI. В stdout пишется результат: список заметок, идентификатор созданной записи, путь к файлу — то, ради чего команду зовут и что скрипты ловят через перенаправление. В stderr — всё остальное: ошибки, предупреждения, прогресс, диагностику. Форматирование здесь то же, что и у fmt.Println, только поток другой: fmt.Fprintln(os.Stderr, ...) и fmt.Fprintf(cmd.ErrOrStderr(), ...). Cobra по умолчанию вежлива: и Error:, и usage при ошибке уходят в stderr, если вывод не переопределён через SetOut — проверьте это, прежде чем парсить вывод своих команд в шелле. Нарушение разделения стоит дорого: notes list 2>/dev/null | grep id перестанет находить идентификаторы, как только одна строка диагностики уедет в stdout, а отладка таких утечек — то ещё удовольствие, потому что отдельно взятый запуск в терминале выглядит правильно.

Куда что печатаем#

ПотокЧто печатаемКто читает
stdoutрезультат: список заметок, id созданной записипользователь и скрипты с перенаправлением
stderrошибки, предупреждения, прогрессчеловек перед экраном и логи CI
код выходаитог: 0 — успех, ненулевой — провалшелл, CI, оркестратор

Соберите тихий main

Впишите поле тишины usage, поток для ошибок и вызов завершения.

rootCmd.Silence = true func main() { if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os., err) os.(1) } }

Соберите обработку ошибки из шести строк

Расставьте: тишина usage, выполнение, проверка ошибки, печать, код выхода, закрытие if.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: ошибки и коды выхода

Термин

SilenceUsage

нажмите, чтобы перевернуть
Определение

Глушит печать usage-страницы при ошибке; --help продолжает работать. Ставьте на корень: скриптам и людям дамп флагов после опечатки не нужен.

1 / 6

Что дальше#

Схема ошибки — RunE, SilenceUsage, код выхода — теперь финальная, осталось научиться её проверять. В уроке про тестирование команд Execute вызывается из теста, вывод ловится буфером вместо терминала, и ошибки сравниваются без os.Exit — ровно поэтому os.Exit и держат в main, подальше от RunE. Как всё это собирается в цельную утилиту, покажет итоговый проект: команды add/list/done с хранилищем, кодами выхода и тестами. Механика оборачивания разобрана отдельно в уроке про ошибки и %w продвинутого курса, обзор остальных тем — на странице Cobra. Как всегда с CLI: код выхода видит только живой процесс — соберите утилиту локально, уроните её специально и проверьте значение $? в шелле.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • RunE возвращает error, main превращает его в код выхода: забытый os.Exit(1) после Execute — нулевой код у упавшей утилиты.
  • SilenceUsage убирает usage-дамп при ошибке, SilenceErrors — печать Error:; печатайте ошибку в одном месте.
  • Код выхода — канал для скриптов и CI: ноль — успех, словарь кодов — для различимых провалов.
  • os.Exit завершает процесс без defer: только в main, никогда в RunE.
  • Оборачивайте ошибки через fmt.Errorf с %w: контекст растёт, errors.Is и errors.As продолжают работать.
  • Результат — в stdout, ошибки — в stderr через fmt.Fprintln(os.Stderr, ...).
  • Две схемы печати: молчаливая Cobra плюс main или болтливая Cobra плюс bare os.Exit(1) — не смешивайте.

Домашнее задание

Проверьте main своей утилиты: обрабатывает ли она ошибку из Execute. Затем включите SilenceUsage на корне, добавьте тип ExitError с полем Code и верните код 3 из команды sync, когда часть заметок не синхронизировалась. Уроните команду специально: неверный флаг, отсутствие хранилища, частичный успех — и выпишите полученные коды выхода через $? в терминале. Отдельно проверьте прерывание: запустите долгую команду и прибейте её Ctrl+C — echo $? покажет 130, то есть 128+2, даже без единой строки вашего кода об ошибках.

Личные заметкиПусто
Хранятся только в вашем браузере (Cobra · Ошибки и коды выхода: SilenceUsage)

Вопросы и ответы по уроку

Почему Cobra печатает usage при каждой ошибке?

Это поведение по умолчанию: ошибка тянет за собой usage-страницу. Задайте SilenceUsage: true на корневой команде — останется одна строка Error:. Флаг на конкретной команде гасит usage только для неё, на корне — для всего дерева.

Как вернуть свой код выхода из команды Cobra?

Не вызывайте os.Exit в RunE. Верните обычную ошибку или тип-обёртку с полем Code, а в main после Execute разберите её через errors.As и вызовите os.Exit с нужным числом. Дефолт для любой ошибки — os.Exit(1).

Куда Cobra печатает ошибку — в stdout или stderr?

В stderr: и сообщение Error:, и usage при ошибке идут в поток ошибок, если вывод не переопределён SetOut. Ваш вывод через fmt.Println уходит в stdout, поэтому ошибки не попадут в перенаправленный результат; для своих сообщений используйте fmt.Fprintln(os.Stderr, ...).

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Bubble Tea25 уроков~10 чпрактика25 уроков уже доступны

Bubble Tea: TUI-приложения в терминале

Интерактивный курс Bubble Tea — TUI-фреймворка на Go: Elm-архитектура, bubbles, Lip Gloss, мышь и клавиатура. 25 уроков с примерами и итоговым приложением.

  • Понимать Elm-архитектуру: Model, Update, View, Init
  • Обрабатывать клавиатуру и мышь в терминале
  • Писать асинхронные операции через tea.Cmd, Batch и Sequence
  • Использовать bubbles: list, textinput, textarea, table, viewport, progress, spinner