Флаги командной строки в Go: что нужно знать до начала
Пакет flag из стандартной библиотеки Go позволяет разобрать аргументы командной строки за десять минут без сторонних зависимостей. Разбираем, как объявить флаги, прочитать их значения, обработать ошибки и не наступить на грабли с порядком вызовов.

Что получится и сколько времени займёт
К концу этого гайда у вас будет CLI-утилита на Go, которая принимает строковые, числовые и булевы флаги, выводит справку по -help и корректно сообщает об ошибках. Весь путь — от пустого файла до рабочего бинарника — занимает около десяти минут. Код написан для Go 1.22, но пакет flag стабилен начиная с Go 1.0, поэтому примеры сработают и на более ранних версиях.
Что нужно иметь до начала
- Установленный Go версии 1.18 или выше (проверьте командой
go version). - Любой текстовый редактор или IDE с поддержкой Go — VS Code с расширением Go, GoLand, даже Vim с
vim-go. - Терминал, в котором вы можете запустить
go run. - Инициализированный модуль: выполните
go mod init cliflagsв пустой директории.
Шаг 1 — объявляем флаги
Пакет flag предлагает два способа объявить флаг: функцию, возвращающую указатель, и функцию, записывающую значение по переданному указателю. Начнём с первого — он короче.
Создайте файл main.go:
package main
import (
"flag"
"fmt"
)
func main() {
// Строковый флаг: имя, значение по умолчанию, описание
host := flag.String("host", "localhost", "адрес сервера")
// Числовой флаг
port := flag.Int("port", 8080, "порт сервера")
// Булев флаг
verbose := flag.Bool("verbose", false, "подробный вывод")
// Обязательный вызов — без него флаги не прочитаются
flag.Parse()
fmt.Printf("host=%s port=%d verbose=%t\n", *host, *port, *verbose)
}
Проверка: запустите в терминале:
go run main.go -host=example.com -port=3000 -verbose
Должна появиться строка:
host=example.com port=3000 verbose=true
Если вывод совпал — шаг пройден.
Почему указатели
flag.String, flag.Int и flag.Bool возвращают *string, *int, *bool. Значение заполняется только после вызова flag.Parse(), поэтому обращаться к *host до парсинга бессмысленно — там будет значение по умолчанию, а не то, что передал пользователь.
Шаг 2 — альтернативный способ: запись по адресу
Если вам удобнее работать с обычными переменными, а не разыменовывать указатели, используйте flag.StringVar, flag.IntVar, flag.BoolVar:
package main
import (
"flag"
"fmt"
)
func main() {
var host string
var port int
var verbose bool
flag.StringVar(&host, "host", "localhost", "адрес сервера")
flag.IntVar(&port, "port", 8080, "порт сервера")
flag.BoolVar(&verbose, "verbose", false, "подробный вывод")
flag.Parse()
fmt.Printf("host=%s port=%d verbose=%t\n", host, port, verbose)
}
Проверка: результат тот же, но host — уже string, а не *string.
Шаг 3 — позиционные аргументы после флагов
Всё, что идёт после последнего распознанного флага (или после --), попадает в flag.Args(). Это полезно, когда утилита принимает и флаги, и список файлов:
flag.Parse()
files := flag.Args() // []string с оставшимися аргументами
fmt.Println("файлы:", files)
Проверка:
go run main.go -verbose file1.txt file2.txt
Вывод должен содержать файлы: [file1.txt file2.txt].
Важный нюанс: пакетflagпрекращает разбор флагов при первом аргументе, который не начинается с дефиса. Поэтомуgo run main.go file1.txt -verboseне включит verbose — флаг окажется вflag.Args(). Это принципиальное отличие от GNU-стиля разбора, где флаги и аргументы можно смешивать.
Шаг 4 — обработка ошибок и справка
По умолчанию, если пользователь передаст неизвестный флаг или флаг с невалидным значением (например, буквы вместо числа для -port), программа напечатает ошибку, покажет справку и завершится с кодом 2. Это поведение управляется через flag.CommandLine.ErrorHandling.
Три варианта обработки ошибок:
| Режим | Поведение при ошибке | Когда полезен |
|---|---|---|
flag.ExitOnError | Печатает ошибку и вызывает os.Exit(2) | По умолчанию для flag.CommandLine, подходит большинству CLI |
flag.ContinueOnError | Возвращает ошибку из Parse(), программа продолжает работу | Когда нужно обработать ошибку самостоятельно, например в тестах |
flag.PanicOnError | Вызывает panic | На практике почти не используется |
Чтобы переопределить текст справки, назначьте свою функцию на flag.Usage:
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(),
"Использование: myapp [флаги] [файлы...]\n\nФлаги:\n")
flag.PrintDefaults()
}
Проверка: запустите go run main.go -help — должен появиться ваш текст, а не стандартный.
Шаг 5 — собственный тип флага
Пакет flag позволяет создать флаг любого типа. Достаточно реализовать интерфейс flag.Value с двумя методами: String() string и Set(string) error. Пример — флаг, принимающий список значений через запятую:
package main
import (
"flag"
"fmt"
"strings"
)
// stringList реализует flag.Value для списка строк
type stringList []string
func (s *stringList) String() string {
return strings.Join(*s, ",")
}
func (s *stringList) Set(value string) error {
*s = strings.Split(value, ",")
return nil
}
func main() {
var tags stringList
flag.Var(&tags, "tags", "теги через запятую: go,cli,tools")
flag.Parse()
fmt.Printf("теги: %v (всего %d)\n", tags, len(tags))
}
Проверка:
go run main.go -tags=go,cli,tools
Ожидаемый вывод: теги: [go cli tools] (всего 3).
Если не получилось
- Флаги не читаются, значения по умолчанию. Убедитесь, что
flag.Parse()вызывается до обращения к значениям. Это самая частая ошибка — объявить флаги и забыть вызвать парсинг. flag provided but not defined: -host. Флаг объявлен после вызоваflag.Parse(), либо опечатка в имени. Все объявления должны идти доParse().invalid value "abc" for flag -port: parse error. Пользователь передал строку в числовой флаг. Это штатное поведение — программа покажет ошибку и справку.- Флаг
-verboseне работает после имени файла. Пакетflagпрекращает разбор при первом не-флаговом аргументе. Флаги должны идти перед позиционными аргументами. go run main.go— ошибка компиляции. Проверьте, что файл начинается сpackage mainи содержитfunc main(). Убедитесь, чтоgo mod initвыполнен в текущей директории.
Сноска: CLI (Command Line Interface) — интерфейс командной строки, способ взаимодействия с программой через текстовые команды в терминале, без графического интерфейса.
Что делать дальше
Пакет flag покрывает базовые сценарии, но у него есть ограничения: нет поддержки подкоманд (вроде git commit), нет автоматического чтения переменных окружения, нет GNU-стиля с двойным дефисом (--host) и коротких однобуквенных флагов (-h). Для простых утилит этого достаточно. Если нужны подкоманды или сложная структура — изучите документацию сторонних пакетов вроде cobra или urfave/cli; но начинайте с flag, чтобы понимать, что именно эти библиотеки абстрагируют.
Следующий практический шаг: напишите утилиту, которая принимает флаг -config с путём к файлу конфигурации и переопределяет отдельные параметры флагами. Это классический паттерн «конфиг + флаги», и пакет flag справляется с ним без сторонних зависимостей.
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: Документация пакета flag — стандартная библиотека Go
Частые вопросы
Можно ли использовать двойной дефис вместо одинарного (-host vs --host)?
Как сделать флаг обязательным?
Что будет, если вызвать flag.Parse() дважды?
Как разобрать флаги в тестах, не затрагивая глобальный flag.CommandLine?
А если у меня более старая версия Go — примеры сработают?
Что делать, если не сработало и ошибка непонятная?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.