Прапорці та середовище¶
Пакет flag зі стандартної бібліотеки навмисно малий. Він добре
обробляє звичайний випадок, має одне неочікуване правило про порядок
аргументів, і цього достатньо для більшості програм.
Визначення та розбір¶
Кожен виклик flag.X повертає вказівник, бо значення не існує,
поки не виконається Parse:
verbose := flag.Bool("verbose", false, "enable verbose output")
port := flag.Int("port", 8080, "port to listen on")
name := flag.String("name", "app", "service name")
timeout := flag.Duration("timeout", 5*time.Second, "request timeout")
flag.Parse()
fmt.Println(*verbose, *port, *name, *timeout)
// без аргументів: false 8080 app 5s
Читання *port до Parse мовчки дає значення за замовчуванням.
Викликайте Parse один раз, на самому початку main, перш ніж
щось читає прапорець.
flag.Duration розбирає ті самі рядки, що й time.ParseDuration зі
статті про час, тож
--timeout 2m працює. Є також форма flag.XVar, яка пише в змінну, що
у вас уже є, — зручно для заповнення структури конфігурації.
Одинарний і подвійний дефіс рівнозначні, а = опційний:
Прапорці зупиняються на першому аргументі, що не є прапорцем¶
Це правило, на якому спотикаються всі. Щоб показати його на
фіксованих списках аргументів, parse нижче щоразу будує новий
flag.FlagSet — приватний набір прапорців, про який розповідає розділ
«Підкоманди» нижче — з одним булевим прапорцем -f:
func parse(args ...string) (bool, []string) {
fs := flag.NewFlagSet("demo", flag.ContinueOnError)
f := fs.Bool("f", false, "force")
fs.Parse(args)
return *f, fs.Args()
}
fmt.Println(parse("-f", "a", "b")) // output: true [a b]
fmt.Println(parse("a", "-f", "b")) // output: false [a -f b]
У другому випадку -f взагалі не розібрано — це просто ще один
позиційний аргумент. У Go немає GNU-стилю перестановки, тож
прапорці мають стояти перед позиційними аргументами. Усе, що
залишається, — це flag.Args(), з flag.NArg() і flag.Arg(i) як
методами доступу.
Булеві прапорці потребують =¶
Булевий прапорець встановлюється своєю присутністю, тож ніколи не
споживає наступний аргумент. Щоб явно передати false, треба
використати =. Тут -d — булевий прапорець, типове значення якого —
true:
fs := flag.NewFlagSet("demo", flag.ContinueOnError)
d := fs.Bool("d", true, "dry run")
fs.Parse([]string{"-d=false"})
fmt.Println(*d) // output: false
-d false встановлює d у true й лишає "false" позиційним
аргументом — саме тому прапорцю зі значенням true за замовчуванням
потрібна ця форма.
Довідка й помилки¶
flag.PrintDefaults виводить згенеровану довідку:
-name string
service name (default "app")
-port int
port to listen on (default 8080)
-timeout duration
request timeout (default 5s)
-verbose
enable verbose output
Третій аргумент кожного визначення — це той текст довідки, тож пишіть його для читача.
При неправильному значенні типовий flag.CommandLine виводить помилку
разом із довідкою й викликає os.Exit(2):
Підкоманди через flag.NewFlagSet¶
Вбудованої підтримки підкоманд немає. flag.NewFlagSet дає кожній
підкоманді власний набір, а ви розподіляєте за os.Args[1]:
serveCmd := flag.NewFlagSet("serve", flag.ExitOnError)
servePort := serveCmd.Int("port", 8080, "port")
migrateCmd := flag.NewFlagSet("migrate", flag.ExitOnError)
migrateDown := migrateCmd.Bool("down", false, "roll back")
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "expected 'serve' or 'migrate'")
os.Exit(2)
}
switch os.Args[1] {
case "serve":
serveCmd.Parse(os.Args[2:])
fmt.Println("serving on", *servePort)
case "migrate":
migrateCmd.Parse(os.Args[2:])
fmt.Println("rolling back:", *migrateDown)
default:
fmt.Fprintf(os.Stderr, "unknown command %q\n", os.Args[1])
os.Exit(2)
}
Режим обробки помилок має значення. flag.ExitOnError завершує роботу
при неправильному прапорці; flag.ContinueOnError повертає помилку,
щоб ви самі вирішили, — саме це робить набір прапорців тестованим.
Змінні середовища¶
fmt.Printf("%q\n", os.Getenv("APP_PORT")) // output: "3000"
fmt.Printf("%q\n", os.Getenv("DEFINITELY_UNSET")) // output: ""
Getenv повертає "" і для "не встановлено", і для "встановлено як
порожнє". Коли ця різниця важлива — порожнє значення означає "явно
вимкнено" — LookupEnv про неї повідомляє:
v, ok := os.LookupEnv("DEFINITELY_UNSET")
fmt.Printf("%q %v\n", v, ok) // output: "" false
os.Setenv("EMPTY", "")
v, ok = os.LookupEnv("EMPTY")
fmt.Printf("%q %v\n", v, ok) // output: "" true
Усе — рядок, тож для чогось іншого потрібен strconv:
Розбирайте конфігурацію один раз під час запуску й голосно
провалюйтесь, а не викликайте Getenv глибоко в коді. Розкидані
пошуки роблять вхідні дані програми неможливими знайти, а описка стає
нульовим значенням замість помилки. Збирання їх в одну структуру
конфігурації, перевірену один раз, — патерн, до якого повертається
тема архітектури.
Стандартна бібліотека не читає файли .env — це зручність від
сторонніх бібліотек.
Коди виходу¶
os.Exit зупиняє негайно. Він не виконує відкладені функції:
Тож ідіома — тримати main тонким і дозволити функції run повертати
помилку:
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
defer cleanup() // виконується
// ...
return nil
}
Помилки йдуть у os.Stderr, вивід — у os.Stdout, тож викликач може
перенаправити один без іншого. log.Fatal пише в stderr і завершує
роботу з кодом 1, з тією самою проблемою defer.
За домовленістю 0 означає успіх, 1 — загальний збій, а 2 —
помилку використання — саме те, що використовує flag.
З досвіду Python:
flag— цеargparseзі значно меншим набором можливостей: немаєnargs, немає підпарсерів, немає взаємовиключних груп, немає автоматичного епілогу--helpпонад згенерований список. Дві реальні поведінкові відмінності — прапорці мають передувати позиційним аргументам, іos.Exitпропускаєdeferтак само, якos._exitпропускаєfinally.
Швидка довідка¶
| Завдання | Виклик |
|---|---|
| визначити | flag.Int("port", 8080, "help") → *int |
| у наявну змінну | flag.IntVar(&cfg.Port, "port", 8080, "help") |
| розібрати | flag.Parse() один раз, на початку main |
| залишок | flag.Args(), flag.NArg(), flag.Arg(i) |
| тривалості | flag.Duration — приймає 2m, 1h30m |
| явний false | -d=false, ніколи -d false |
| підкоманди | flag.NewFlagSet(name, flag.ExitOnError) + os.Args[1] |
| тестований розбір | flag.ContinueOnError |
| середовище, може бути порожнім | os.Getenv(k) |
| відрізнити невстановлене | os.LookupEnv(k) → (value, ok) |
| середовище в число | strconv.Atoi |
| провалитись | пишіть в os.Stderr, тоді os.Exit(1) — пропускає defer |