---
title: "Флаги и структура конфига: паттерн binding"
description: "Cobra флаги в структуру Config: паттерн binding — биндинг в init, дефолты из конструктора, валидация и план приоритетов флаг, конфиг, дефолт. Пример на Go."
course: "Cobra: CLI-приложения на Go"
url: "https://golangmaster.ru/courses/cobra/lessons/06-flagi-i-konfig/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «Cobra: CLI-приложения на Go», урок 6 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/cobra/lessons/06-flagi-i-konfig/*

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

Посмотрите на команду 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.

> **Нулевая структура — тоже план.** Если конструктор не написан, serveConfig{} даст порту 0, а строкам пустоту — и Var-методы объявят дефолтами именно эти нули: справка будет врать, а валидация выловит не сразу. Конструктор с дефолтами — не церемония, а единственное место, где утилита договаривается с пользователем о поведении без настроек. Пустой структурный литерал в биндинге — запах.

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

| Слой | Кто задаёт | Сила | Когда появится в курсе |
| --- | --- | --- | --- |
| Дефолт | 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 ошибки не пробрасывает — об этом отдельный разговор в уроке про коды выхода.

> **Добавили поле — добавьте и биндинг.** Классическая ошибка паттерна: поле в структуре есть, а Var-метода для него нет. Компилятор молчит — поле просто всегда дефолтное, и баг живёт неделями, пока кто-то не наберёт несуществующий флаг. Я держу bindFlags рядом с определением структуры и читаю их сверху вниз: поле — строка биндинга. Дифф из ревью, где новая строка в структуре осталась без пары в биндинге, — стоп-комментарий.

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

С ростом утилиты возникает соблазн сделать один гигантский 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?

- Внутри RunE, перед чтением полей
- ✅ В init() или конструкторе команды — до Execute
- В методе Validate перед валидацией
- В любом месте: pflag читает объявления лениво

Разбор строки запуска происходит в Execute до вызова RunE, поэтому все флаги должны быть зарегистрированы заранее — в init() файла команды или в конструкторе. Объявление в RunE ловится ошибкой unknown flag.

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

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

- Компиляция упадёт: не все поля забиндены
- При запуске Cobra ответит unknown flag --debug
- ✅ Компиляция пройдёт, поле всегда останется дефолтным
- pflag сам создаст флаг по имени поля

Незаполненный биндинг — тихая ошибка: код компилируется, справка флага не знает, значение поля не меняется никогда. Поэтому пара «поле — строка биндинга» проверяется глазами на ревью, а не компилятором.

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

```go
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)
}
```

**Что выведет программа?**

- ✅ 127.0.0.1:9000 — флаг перезаписал поле структуры
- 127.0.0.1:8080 — дефолт не перезаписался
- Паника: структура создана до объявления флагов
- Пустая строка и 0 — cfg ещё не инициализирован

Структура создаётся при инициализации пакета, Var-методы биндят адреса её полей, а дефолт в Var — тот же cfg.Port. Флаг -p 9000 перезаписывает поле, и RunE видит 9000. Порядок объявления cfg и serveCmd в файле не важен: Go инициализирует переменные по зависимостям, init() идёт последним.

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

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

```go
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
        }
}
```

Подсказка: Поле для адреса называется Host; флаг с шорткатом -p объявляет IntVarP; ошибка принимается в переменную err.

Правильные ответы в пропусках: `Host`, `IntVarP`, `err`

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

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

Правильный порядок строк:

1. `type serveConfig struct {`
2. `  Port  int`
3. `  Watch bool`
4. `}`
5. `var cfg = serveConfig{Port: 8080, Watch: true}`
6. `func init() { serveCmd.Flags().IntVar(&cfg.Port, "port", cfg.Port, "порт") }`

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

- **Config-структура** — Тип с именованными полями под настройки одной команды: Port int, Host string. Заменяет россыпь глобальных флаговых переменных.
- **bindFlags(cmd)** — Метод, который вешает Var-методы pflag на поля структуры. Живёт в init() или конструкторе — до Execute.
- **defaultServeConfig()** — Конструктор с дефолтами: единственное место, где задаются значения по умолчанию. Его же значения идут в Var-методы как дефолт.
- **RunE** — Вариант Run, возвращающий error: валидация конфига и ошибки настройки пробрасываются в Cobra, которая печатает их и уходит с кодом 1.
- **План приоритетов** — Дефолт — слабее всех, над ним конфиг-файл, над ним переменные окружения, сильнее всех флаг. Полная реализация — Viper и BindPFlag в уроках 07-10.

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

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

### Что дальше

Структура с дефолтами и флагами закрывает два слоя из плана приоритетов; третий — конфиг-файл — потребует библиотеки, и следующий урок, [знакомство с Viper](/courses/cobra/lessons/07-viper-znakomstvo/), покажет, что она решает и когда её брать не нужно. Полная связка флагов с Viper и расстановка слоёв — в уроке про [приоритеты конфигурации](/courses/cobra/lessons/10-prioritety-konfiguraciya/), а пример того же паттерна в другом фреймворке — в уроке про [zap.Config](/courses/zap/lessons/07-zap-config/), где YAML-конфиг собирается в структуру логгера. Обычное напоминание: паттерн проверяется только в терминале — соберите serve, прогоните его с -p 9000 и без флагов, сравните вывод; песочница сайта Cobra не исполняет.

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

- Паттерн 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?

Завести тип с полями под настройки и в 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 вручную.
