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

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

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

Урок 6 из 25 ~18 мин +50 XP

Флаги и структура конфига: паттерн binding

Cobra флаги в структуру собирает паттерн binding: один тип Config на команду, биндинг полей в init(), дефолты в конструкторе и валидация перед запуском. В уроке — полный пример для команды serve, план приоритетов флаг — конфиг — дефолт и список ошибок, на которых обычно спотыкаются.

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

Посмотрите на команду serve в любом реальном CLI: порт, адрес, таймауты, режим отладки, путь к хранилищу, лимиты. К пятому флагу Run превращается в функцию, которая держит в голове пять глобальных переменных, объявленных в другом файле, а к восьмому вы ловите себя на том, что боитесь переименовать поле. Сигнатура RunE фиксирована — cmd и args, — так что внутрь не протолкнёшь ни структуру, ни конструктор. Решение старое, как сам Go: собрать настройки в один тип, а флаги биндить прямо в его поля. Тогда у команды появляется собственный мини-конфиг: конструктор с дефолтами, метод биндинга, валидация — и RunE читает одно значение вместо пятнадцати переменных. Этот паттерн называют binding, и в этой статье разберём его от структуры до ошибок.

Паттерн binding за пять шагов#

  1. Завести структуру с полем под каждую настройку команды
  2. Написать конструктор defaultServeConfig с рабочими дефолтами
  3. Создать пакетную переменную cfg = defaultServeConfig()
  4. В init() биндить поля Var-методами, дефолт брать из структуры
  5. В RunE вызвать cfg.Validate() и работать только с cfg

Config-структура: флаги в структуру, одно поле — один флаг#

Основа паттерна — обычная структура с именованными полями. Никаких map[string]any: я с картами настроек наигрался в старых проектах и больше не борюсь с искушением — опечатка в ключе карты живёт до рантайма, опечатка в поле структуры ловится компилятором на месте. Каждому флагу соответствует своё поле с честным типом: Port int, Host string, Watch bool. Рядом живёт конструктор defaultServeConfig(), который возвращает структуру с дефолтами — это единственное место, где значения по умолчанию задаются в коде. Третьий элемент — метод bindFlags(cmd), который навешивает Var-методы на поля структуры. Связка получается замкнутая: поле — строка биндинга — флаг в справке. Компилятор следит за типами, справка печатается сама, а тест может собрать конфиг руками, без os.Args.

serveConfig: структура, конструктор, биндинг

GO
package cmd

import (
        "fmt"

        "github.com/spf13/cobra"
)

// serveConfig — все настройки команды serve в одном типе.
type serveConfig struct {
        Port    int
        Host    string
        Watch   bool
        Timeout int // секунды
}

// defaultServeConfig — единственное место с дефолтами.
func defaultServeConfig() serveConfig {
        return serveConfig{Port: 8080, Host: "127.0.0.1", Watch: false, Timeout: 30}
}

// cfg — единственный экземпляр, который читает RunE.
var cfg = defaultServeConfig()

var serveCmd = &cobra.Command{
        Use:   "serve",
        Short: "Запустить сервер предпросмотра",
        RunE: func(cmd *cobra.Command, args []string) error {
                fmt.Printf("%s:%d (watch=%t)\n", cfg.Host, cfg.Port, cfg.Watch)
                return nil
        },
}

func init() {
        // Биндинг полей структуры в флаги. Дефолт в Var-методе
        // берём из самой структуры — значение задаётся один раз.
        serveCmd.Flags().IntVarP(&cfg.Port, "port", "p", cfg.Port, "порт HTTP-сервера")
        serveCmd.Flags().StringVar(&cfg.Host, "host", cfg.Host, "адрес привязки")
        serveCmd.Flags().BoolVarP(&cfg.Watch, "watch", "w", cfg.Watch, "следить за файлами")
        serveCmd.Flags().IntVar(&cfg.Timeout, "timeout", cfg.Timeout, "таймаут, сек")

        rootCmd.AddCommand(serveCmd)
}

Структура создаётся один раз при инициализации пакета; Var-методы биндят адреса её полей в флаги. Ключевой трюк — дефолт в Var-методе берётся из самой структуры: значение по умолчанию задаётся единожды в defaultServeConfig, и расхождения между структурой и справкой не бывает. RunE читает только cfg — глобальные флаговые переменные исчезли.

Почему биндинг — в init(), а не в RunE#

Самый частый вопрос по паттерну: почему бы не собрать структуру прямо внутри RunE? Потому что к моменту вызова RunE разбор строки запуска уже закончен. Cobra парсит флаги в Execute — до Run, — и каждый флаг, который она встретила, должен быть зарегистрирован к этому моменту. Флаг, объявленный внутри RunE, объявлен слишком поздно: на нём разбор падает с unknown flag ещё до запуска команды. Поэтому биндинг живёт в init() файла команды — именно так раскладывает код генератор cobra-cli, — либо в конструкторе команды, который вызывается до AddCommand. Проверить это легко только локально: соберите утилиту и попробуйте оба варианта, песочница сайта строку запуска не исполняет. Ручной вызов cmd.ParseFlags внутри RunE тоже возможен, но нужен редко — это костыль для динамически объявляемых флагов, и в обычном CLI ему делать нечего.

Дефолты и план приоритетов: флаг, конфиг, дефолт#

Пока у нас два источника настроек, и иерархия простая: дефолт из конструктора перекрывается флагом, если пользователь его передал. pflag делает это бесплатно — пользовательское значение пишется в то же поле, куда смотрит Var-метод. Но CLI зреют, и между дефолтом и флагом встаёт конфиг-файл: пользователь один раз прописал порт в notes.yaml и не хочет набирать --port 8080 каждый запуск. План приоритетов, к которому мы придём к десятому уроку: дефолт — самый слабый слой, над ним конфиг-файл, над ним переменные окружения, и сильнее всех флаг, набранный в терминале. Чтобы флаг честно побеждал конфиг, связку берут на себя Viper и BindPFlag — о них пойдут седьмой и десятый уроки. Пока запомните принцип: чем ближе источник к пользователю, тем он сильнее, и структура с дефолтами — фундамент, на котором слои раскладываются без переписывания RunE.

Слои настроек: пока два из четырёх#

СлойКто задаётСилаКогда появится в курсе
ДефолтdefaultServeConfig()слабее всехэтот урок
Конфиг-файлYAML или TOML на дискевыше дефолта, ниже флагауроки 08 и 10
Переменные окруженияshell или docker-composeвыше конфигаурок 09
Флагпользователь в терминалесильнее всехэтот урок

Validate: валидация связности настроек

GO
// Validate проверяет связность настроек до запуска сервера.
func (c serveConfig) Validate() error {
        if c.Port <= 0 || c.Port > 65535 {
                return fmt.Errorf("порт %d вне диапазона 1-65535", c.Port)
        }
        if c.Timeout < 1 {
                return fmt.Errorf("таймаут %d сек слишком мал", c.Timeout)
        }
        if c.Watch && c.Host == "0.0.0.0" {
                return fmt.Errorf("watch на 0.0.0.0 опасен: вас увидит вся сеть")
        }
        return nil
}

// В RunE проверка занимает одну строку:
//     if err := cfg.Validate(); err != nil { return err }

Validate ловит то, чего не умеет MarkFlagRequired: не наличие значения, а его связность. Порт в диапазоне, таймаут осмысленный, опасные комбинации выключены. RunE возвращает error — Cobra напечатает сообщение и уйдёт с кодом 1. Метод вешается на значение, а не на указатель: конфиг в валидации не меняется.

Валидация настроек до старта#

Валидация — недооценённая часть паттерна. MarkFlagRequired отвечает только на вопрос «флаг передан вообще», а большинство ошибок настройки — про значения: порт занят или вне диапазона, таймаут в наносекундах из-за опечатки в единицах, watch на адресе 0.0.0.0, который выставляет сервер наружу. Собирать эти проверки в RunE — значит утопить бизнес-логику под парсингом; с Config-структурой им естественно живётся в методе Validate, который вызывается первой строкой RunE. Дополнительный бонус: валидатор можно гонять в тестах на готовых структурах — без Execute, без os.Args, без сборки бинарника. Отсюда и выбор RunE вместо Run: команда, которая может отклонить настройку, обязана уметь возвращать ошибку, а Run ошибки не пробрасывает — об этом отдельный разговор в уроке про коды выхода.

Подкоманды со своими конфигами#

С ростом утилиты возникает соблазн сделать один гигантский Config на всё приложение — не поддавайтесь. У каждой подкоманды своя структура: serveConfig для serve, listConfig для list, и пересечение между ними — только персистентные флаги корня вроде --verbose, у которых своя маленькая структура или одиночные переменные. Общий конфиг оправдан в одном случае: несколько подкоманд одного семейства шарят один и тот же набор настроек — тогда общий тип в отдельном файле пакета cmd уместен. Связку между конфигом подкоманды и корневым состоянием делают параметры конструктора: подкоманда получает нужный кусок при создании, а не лезет в чужие пакеты за глобальной переменной. Такая изоляция бесплатна, пока дерево маленькое, и спасает, когда оно вырастает: правка настроек serve перестаёт задевать list чисто механически — файлы разные.

Признаки, что пора на паттерн binding#

  • У команды больше пяти флагов, и Var-методы перестали помещаться на экран
  • Два файла объявляют и читают одни и те же глобальные переменные
  • Тест Run требует ручной сборки os.Args
  • В справке появились настройки, которых в коде уже нет
  • Новая подкоманда копирует биндинг старой почти целиком

Биндинг прямо в доменную структуру#

Когда доменный тип совпадает с набором настроек, промежуточный Config-тип не нужен: биндите флаги прямо в его поля. У репортера заметок есть тип Report с полями Limit, Author, Since — флаг --limit биндится в report.Limit, и слой передачи значений исчезает как класс. Нюанс один: доменный тип обязан жить в том же пакете, что и команда, либо в пакете, который команда может импортировать; циклические импорты в Go запрещены, и это само решает, чья ответственность где. Паттерн ломается, когда у команды настроек больше, чем полей у доменного типа: --verbose не про отчёт, а про вывод. В таких случаях комбинируйте: часть флагов в доменную структуру, часть — в маленький cmdConfig рядом. Я так делаю всегда: перебор абстракций ради одного булева флага дороже, чем два биндинга.

Тесты: конфиг без os.Args#

Скрытая выгода паттерна проявляется в тестах. До биндинга протестировать Run означало собрать os.Args руками и вызвать Execute — хрупко и многословно. С Config-структурой тест собирает структуру напрямую: cfg := serveConfig{Port: 8080, Watch: true}, прогоняет Validate и вызывает функцию serve с конфигом. Команда — тонкая оболочка над логикой, логика тестируется без CLI. Если вы уже проходили тестирование в продвинутом курсе, сравните оба подхода на своём проекте: разница в количестве кода на один тест — примерно в три раза. Мы к тестам Cobra-команд вернёмся ближе к финалу курса, когда дерево утилиты соберётся целиком; пока запомните приём — команда читает структуру, структура собирается где угодно.

Проверка: место биндинга

Где биндить флаги в поля Config-структуры, чтобы пользовательские значения дошли до RunE?

Проверка: пропущенный биндинг

В serveConfig добавили поле Debug bool, но в bindFlags строку для него забыли. Что произойдёт?

Что напечатает notes serve -p 9000

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

package cmd

import "fmt"

type serveConfig struct {
        Port int
        Host string
}

func defaultServeConfig() serveConfig {
        return serveConfig{Port: 8080, Host: "127.0.0.1"}
}

var cfg = defaultServeConfig()

var serveCmd = &cobra.Command{
        Use: "serve",
        RunE: func(cmd *cobra.Command, args []string) error {
                fmt.Printf("%s:%d\n", cfg.Host, cfg.Port)
                return nil
        },
}

func init() {
        serveCmd.Flags().IntVarP(&cfg.Port, "port", "p", cfg.Port, "порт")
        serveCmd.Flags().StringVar(&cfg.Host, "host", cfg.Host, "адрес")
        rootCmd.AddCommand(serveCmd)
}

Восстановите паттерн binding

Впишите имя поля, метод биндинга с шорткатом и переменную для ошибки.

func defaultServeConfig() serveConfig { return serveConfig{Port: 8080, : "127.0.0.1", Watch: false, Timeout: 30} } func init() { serveCmd.Flags().(&cfg.Port, "port", "p", cfg.Port, "порт") if := cfg.Validate(); err != nil { return err } }

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

Расставьте строки: тип, два поля, закрытие типа, инициализация с дефолтом и однострочный init с биндингом.

Строки

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

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

Карточки: паттерн binding

Термин

Config-структура

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

Тип с именованными полями под настройки одной команды: Port int, Host string. Заменяет россыпь глобальных флаговых переменных.

1 / 5

Config и Viper: стыковка двух миров#

Со следующего урока в проекте появится Viper, и вопрос «а куда теперь девается cfg?» закономерен. Ответ: структура остаётся, а наполняется иначе. Пока источника два — дефолт и флаг, — биндинг из этого урока работает как есть. Когда добавятся файл и окружение, порядок наполнения станет таким: defaultServeConfig задаёт базу, Viper читает конфиг и окружение, и в конце срабатывает BindPFlag или ручной перенос флагов — самый сильный слой. В десятой функции Unmarshal соберёт реестр Viper в структуру одним вызовом, и тогда bindFlags либо перепишется на BindPFlag, либо останется командам без конфига. Важно, что интерфейс RunE не меняется ни на каком шаге: команда по-прежнему читает структуру. Именно поэтому паттерн binding стоит ставить сразу, а не «когда понадобится конфиг»: миграция слоёв не трогает ни одну команду.

Что дальше#

Структура с дефолтами и флагами закрывает два слоя из плана приоритетов; третий — конфиг-файл — потребует библиотеки, и следующий урок, знакомство с Viper, покажет, что она решает и когда её брать не нужно. Полная связка флагов с Viper и расстановка слоёв — в уроке про приоритеты конфигурации, а пример того же паттерна в другом фреймворке — в уроке про zap.Config, где YAML-конфиг собирается в структуру логгера. Обычное напоминание: паттерн проверяется только в терминале — соберите serve, прогоните его с -p 9000 и без флагов, сравните вывод; песочница сайта Cobra не исполняет.

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

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

Вывод

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

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

  • Паттерн binding: настройки команды — один тип Config, поля биндятся в флаги Var-методами.
  • Дефолты задаются один раз в конструкторе defaultServeConfig и переиспользуются в Var-методах.
  • Биндинг живёт в init() или конструкторе команды: внутри RunE разбор уже закончен.
  • RunE читает только cfg — глобальные флаговые переменные исчезают.
  • Validate() ловит связность значений, которую не умеет MarkFlagRequired.
  • Поле без строки биндинга — тихая ошибка: компилятор молчит, поле всегда дефолтное.
  • План приоритетов: дефолт < конфиг-файл < env < флаг; полную связку даст Viper.

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

Перепишите serve из урока на паттерн binding: структура с полями Port, Host, Timeout, конструктор с дефолтами и Validate, которая отбрасывает порт вне 1-65535. Проверьте: notes serve без флагов печатает дефолты, -p 9000 перезаписывает порт, а некорректный таймаут возвращается ошибкой с кодом выхода 1.

Личные заметкиПусто
Хранятся только в вашем браузере (Cobra · Флаги и структура конфига: паттерн binding)

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

Как биндить флаги в структуру в Cobra?

Завести тип с полями под настройки и в init() команды вызвать Var-методы на адреса полей: cmd.Flags().IntVarP(&cfg.Port, "port", "p", cfg.Port, "порт"). В RunE читать структуру — pflag уже записал туда пользовательские значения или дефолты.

Можно ли биндить флаги внутри RunE?

Нет: разбор строки запуска происходит в Execute до вызова RunE, и незарегистрированный флаг роняет разбор с unknown flag. Биндинг выполняют в init() файла команды или в конструкторе, вызываемом до AddCommand.

Чем RunE лучше Run для команд с конфигом?

RunE возвращает error: валидатор конфига может отклонить запуск сообщением и кодом выхода 1. Run ошибок не пробрасывает, поэтому отказ по настройке пришлось бы обрабатывать через fmt.Println и os.Exit вручную.

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

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

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

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

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

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