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

Почему flag кажется простым, но подводит
Стандартный пакет flag в Go (версия 1.22+) умещается в сотню строк документации. Объявил флаг, вызвал flag.Parse(), получил значение — готово. Но именно эта простота провоцирует ошибки: новички пропускают Parse(), путают указатели со значениями, не обрабатывают позиционные аргументы и получают программу, которая молча работает не так, как задумано. Ниже — шесть конкретных промахов и способы их не допустить.
Что нужно до начала
- Go 1.22 или новее (проверьте:
go version). - Любой текстовый редактор и терминал.
- Базовое понимание указателей в Go: если
*intи&xпока не знакомы, стоит прочитать раздел «Pointers» в Tour of Go.
Ошибка 1: забыть вызвать flag.Parse()
Самая частая и самая тихая проблема. Флаги объявлены, но flag.Parse() нигде не вызван. Программа компилируется, запускается — и все флаги имеют дефолтные значения, что бы пользователь ни передал в командной строке.
package main
import (
"flag"
"fmt"
)
func main() {
port := flag.Int("port", 8080, "порт для прослушивания")
// flag.Parse() — забыли
fmt.Println("Порт:", *port) // всегда 8080
}
Запустите с go run main.go -port 3000 — вывод всё равно Порт: 8080. Никакой ошибки, никакого предупреждения.
Как исправить
Добавьте flag.Parse() после всех объявлений флагов и до первого обращения к их значениям:
func main() {
port := flag.Int("port", 8080, "порт для прослушивания")
flag.Parse()
fmt.Println("Порт:", *port) // теперь отражает переданное значение
}
Проверка: запустите go run main.go -port 3000 и убедитесь, что вывод — Порт: 3000.
Ошибка 2: обращение к значению флага вместо разыменования указателя
Функции flag.Int, flag.String, flag.Bool возвращают указатели. Новичок пишет fmt.Println(port) и видит адрес в памяти вместо числа.
port := flag.Int("port", 8080, "порт")
flag.Parse()
fmt.Println(port) // 0xc0000b6010 — адрес, не значение
fmt.Println(*port) // 8080 — значение
Альтернатива: flag.IntVar
Если указатели раздражают, используйте варианты с суффиксом Var — они записывают результат в обычную переменную:
var port int
flag.IntVar(&port, "port", 8080, "порт для прослушивания")
flag.Parse()
fmt.Println(port) // 8080 или то, что передал пользователь
Ошибка 3: объявление флага после Parse()
Порядок имеет значение. Если объявить флаг после flag.Parse(), парсер о нём не знает, значение останется дефолтным, а переданный пользователем аргумент попадёт в flag.Args() — или вызовет ошибку «flag provided but not defined».
func main() {
flag.Parse()
// объявляем после Parse — бессмысленно
verbose := flag.Bool("verbose", false, "подробный вывод")
fmt.Println(*verbose) // всегда false
}
Правило простое: сначала все flag.*-объявления, потом единственный flag.Parse().
Ошибка 4: путаница между флагами и позиционными аргументами
Пакет flag разделяет аргументы командной строки на флаги (начинаются с - или --) и позиционные (всё остальное). Позиционные аргументы доступны через flag.Args() и flag.Arg(n) — но только после flag.Parse(). Частая ошибка: пытаться читать имя файла из os.Args[1] вместо flag.Arg(0), когда в командной строке есть и флаги, и позиционные аргументы.
// go run main.go -verbose input.txt
func main() {
verbose := flag.Bool("verbose", false, "подробный вывод")
flag.Parse()
// Неправильно: os.Args[1] — это "-verbose", а не "input.txt"
// Правильно:
if flag.NArg() == 0 {
fmt.Println("Укажите имя файла")
return
}
filename := flag.Arg(0) // "input.txt"
fmt.Printf("verbose=%v, file=%s\n", *verbose, filename)
}
Пакетflagпрекращает парсить флаги при первом аргументе, который не начинается с дефиса (или после специального--). Всё, что идёт после, — позиционные аргументы. Это поведение отличается от GNU-стиля, где флаги можно ставить после позиционных аргументов.
Ошибка 5: молчаливый дефолт вместо обязательного флага
В flag нет понятия «обязательный флаг». Если пользователь не передал -config, значение просто останется дефолтным — пустой строкой, нулём или false. Программа продолжит работу с бессмысленным значением.
Как обработать
func main() {
config := flag.String("config", "", "путь к файлу конфигурации (обязательный)")
flag.Parse()
if *config == "" {
fmt.Fprintln(os.Stderr, "Ошибка: флаг -config обязателен")
flag.Usage() // выводит справку по всем флагам
os.Exit(1)
}
fmt.Println("Конфиг:", *config)
}
Проверка: запустите без -config — программа выведет ошибку и справку, а не продолжит молча.
Ошибка 6: булев флаг с явным значением
Новички ожидают, что -verbose true установит флаг в true. Но пакет flag разбирает булевы флаги иначе: -verbose без значения — это true, а -verbose=true и -verbose=false работают только через знак =. Форма -verbose true (через пробел) интерпретирует true как позиционный аргумент, а не как значение флага.
| Запись | Значение verbose | Позиционные аргументы |
|---|---|---|
-verbose | true | нет |
-verbose=true | true | нет |
-verbose=false | false | нет |
-verbose true | true | true (строка) |
-verbose false | true | false (строка) |
Последние две строки таблицы — источник трудноуловимых багов. Если вашей утилите нужен явный -verbose=false, документируйте синтаксис с =.
Когда стандартного flag недостаточно
Пакет flag намеренно минималистичен. Вот чего в нём нет:
- Подкоманд (вроде
git commit,docker run). - POSIX-совместимого разбора (
--long-flagс двумя дефисами, короткие однобуквенные флаги через одиночный дефис). - Автоматической валидации обязательных флагов.
- Группировки и зависимостей между флагами.
Для более сложных CLI существуют сторонние библиотеки — cobra, urfave/cli, kong. Выбор между ними — отдельная тема, здесь лишь обозначим: если вашей утилите нужны подкоманды или POSIX-стиль, стандартный flag потребует много обвязки, которую проще не писать руками.
Сноска: позиционный аргумент — аргумент командной строки, который не является флагом (не начинается с -). Типичный пример: имя файла в cat file.txt. В пакете flag позиционные аргументы доступны через flag.Args() после вызова flag.Parse().
Что делать дальше
Проверьте свои существующие CLI-утилиты на Go по списку выше — особенно на ошибки 1 и 5: пропущенный Parse() и отсутствие валидации обязательных флагов. Напишите для каждого флага тест, который вызывает программу без аргумента и проверяет код возврата. Если утилита доросла до подкоманд, посмотрите документацию cobra или urfave/cli — и не пытайтесь имитировать подкоманды через позиционные аргументы вручную.
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: Документация пакета flag — стандартная библиотека Go, Go by Example: Command-Line Flags
Частые вопросы
Чем flag.Int отличается от flag.IntVar?
Поддерживает ли пакет flag двойной дефис (--flag)?
Как сделать флаг обязательным?
Что если я передам флаг, который не объявлен?
Что если у меня другая версия Go — код будет работать?
Можно ли использовать flag в тестах?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.