os.Args
os.Args это слайс строк. os.Args[0] это имя программы, остальное это аргументы ровно в том виде, в каком их передала оболочка.
В панели Args редактора каждое поле это один аргумент, который передаётся как есть. Введите hello, two words и -v в три поля: программа увидит три аргумента, и two words останется одним аргументом с пробелом внутри. В терминале разбиение делает оболочка, поэтому то же самое выглядит как go run . hello "two words" -v.
Всегда проверяйте len(os.Args) перед обращением по индексу. os.Args[1] без аргументов паникует с index out of range [1] with length 1.
Аргументы это строки. Числа преобразуйте через strconv.Atoi или strconv.ParseFloat и обрабатывайте ошибку, ведь пользователи вводят что угодно.
Пакет flag
Для опций вроде -port 8080 -verbose используйте flag. Он разбирает аргументы, преобразует типы, сообщает об ошибках и генерирует справку.
Без аргументов программа один раз печатает hello, world. В панели Args введите -name и Gopher в два поля, а затем ещё -count=3, -loud и extra в три. Программа трижды напечатает HELLO, GOPHER! и remaining args: [extra].
Как работают флаги:
- Каждая функция объявления (
flag.String,flag.Int,flag.Bool,flag.Float64,flag.Duration,flag.Uint64...) принимает имя, значение по умолчанию и строку справки и возвращает указатель. Читайте значение через*nameпослеflag.Parse(). - Формы с
Varпривязываются к уже существующей переменной:flag.IntVar(&cfg.Port, "port", 8080, "port"). Для структуры конфигурации так аккуратнее. - Пользователи могут писать
-name value,-name=value,--name valueили--name=value. Go не различает один и два дефиса. - Булевым флагам для значения нужен
=.-loudставит true,-loud=falseставит false, а-loud falseставит true и оставляетfalseпозиционным аргументом. - Разбор останавливается на первом аргументе, который не флаг (или на
--).prog file.txt -vсчитает-vпозиционным аргументом. Ставьте флаги первыми. flag.Args()возвращает оставшиеся позиционные аргументы,flag.NArg()их число, аflag.Arg(i)один из них.
Неизвестный флаг или неверное значение печатают ошибку и справку и завершают программу с кодом 2. -h или -help печатает справку и завершает с кодом 0 (начиная с Go 1.15). Текст справки генерируется из ваших объявлений:
Usage of greet:
-count int
how many times (default 1)
-delay duration
pause between greetings, e.g. 10ms
-loud
shout the greeting
-name string
who to greet (default "world")
Присвойте flag.Usage свою функцию, чтобы напечатать собственный заголовок перед вызовом flag.PrintDefaults().
Подкоманды через FlagSet
У утилит вроде git commit -m msg есть подкоманды со своими флагами. Создайте flag.FlagSet на каждую подкоманду и выбирайте нужную через switch по первому аргументу:
flag.ContinueOnError заставляет Parse вернуть ошибку, а не завершать программу, и это оставляет функцию тестируемой. run принимает аргументы параметром, а не читает os.Args, поэтому тест может вызвать run([]string{"list", "-all"}) напрямую. Попробуйте в панели Args list и -all или delete, чтобы увидеть путь с ошибкой.
Для больших CLI с вложенными командами, автодополнением в оболочке и генерируемой документацией большинство проектов используют сторонний github.com/spf13/cobra. Для маленьких утилит стандартного пакета flag вполне хватает.
Переменные окружения
os.Getenvвозвращает""и когда переменной нет, и когда она задана пустой строкой.os.LookupEnvих различает.- Значения всегда строки. Преобразуйте и проверяйте их при запуске и падайте с понятным сообщением, а не посреди обработки запроса.
os.Setenvвлияет на текущий процесс и на дочерние процессы, запущенные позже. Окружение оболочки, которая вас запустила, он изменить не может.os.Environ()возвращает все переменные в виде строк"KEY=value".
Распространённая схема конфигурации: флаги для того, что вводит человек, переменные окружения для настроек развёртывания (их задают контейнерные платформы), причём флаги переопределяют окружение, а окружение переопределяет значения по умолчанию.
Коды выхода и os.Exit
Программа на Go завершается с кодом 0, когда main возвращает управление. os.Exit(code) сразу завершает процесс с этим кодом. По соглашению 0 это успех, 1 общая ошибка, а 2 ошибка использования (так делает пакет flag).
os.Exit не выполняет отложенные функции. Файлы не сбрасываются на диск, а очистка в defer пропускается. log.Fatal вызывает os.Exit(1) с тем же эффектом. Держите os.Exit в одном месте, в конце main, а саму программу поместите в функцию run, которая возвращает ошибку, как в примере с подкомандами:
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
Пишите сообщения об ошибках в os.Stderr, а не в os.Stdout, чтобы они оставались видны, когда вывод перенаправлен в файл или передан другой команде. Неперехваченная паника завершает программу с кодом 2.
Частые ошибки
- Обращение к
os.Argsпо индексу без проверки длины. Отсутствующие аргументы вызывают панику. - Чтение флагов до
flag.Parse(). Вы получите значения по умолчанию. - Забытая
*.fmt.Println(port)печатает адрес вроде0xc000012345, а не значение. - Флаги после позиционных аргументов.
prog input.txt -vне разберёт-v. -verbose falseдля булева флага. Пишите-verbose=false.os.Exitилиlog.Fatalглубоко внутри программы. Отложенная очистка не выполнится, а код нельзя будет протестировать. Возвращайте ошибки наверх доmain.
Часто задаваемые вопросы
Как получить аргументы командной строки в Go?
os.Args это []string, где под индексом 0 лежит имя программы, а за ним аргументы. os.Args[1:] это аргументы, которые ввёл пользователь. Проверяйте len(os.Args) перед обращением по индексу, иначе программа запаникует при отсутствии аргумента.
Как пользоваться пакетом flag в Go?
Объявите флаги, вызовите flag.Parse(), затем читайте их: port := flag.Int("port", 8080, "port to listen on"), flag.Parse(), fmt.Println(*port). Функции возвращают указатели. Пользователи пишут -port=9000, -port 9000 или --port 9000, а -h печатает сгенерированную справку.
Как прочитать переменную окружения в Go?
os.Getenv("HOME") возвращает значение или пустую строку, если переменная не задана. Чтобы отличить «не задана» от «задана пустой», используйте v, ok := os.LookupEnv("HOME"). os.Setenv меняет окружение текущего процесса и дочерних процессов, запущенных после этого.
Выполняет ли os.Exit отложенные функции в Go?
Нет. os.Exit сразу завершает процесс с указанным кодом, и отложенные вызовы не выполняются, так что буферизованный вывод может пропасть, а файлы не сбросятся на диск. Распространённый шаблон: func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) } }, где вся настоящая работа и все defer находятся внутри run.