Перейти до основного вмісту
Модуль 10 · Урок 4 із 549/50 у курсі~18 min
Модульні уроки (4/5)

Міні-проект: CLI з прапорцями

Давайте об’єднаємо будівельні блоки попередніх модулів, щоб побудувати невеликий ідіоматичний CLI: аналіз прапора за допомогою пакета flag, обробка позиційних аргументів, помилки на os.Stderr та правильний код виходу.

Пакет flag

Go
package main

import (
    "flag"
    "fmt"
    "os"
)

func main() {
    n := flag.Int("n", 1, "number of repetitions")
    upper := flag.Bool("u", false, "print in uppercase")
    flag.Parse()

    args := flag.Args() // remaining positional arguments
    if len(args) < 1 {
        fmt.Fprintln(os.Stderr, "usage: echo [-n N] [-u] <text>")
        os.Exit(2)
    }

    text := args[0]
    if *upper {
        text = strings.ToUpper(text)
    }
    for i := 0; i < *n; i++ {
        fmt.Println(text)
    }
}

Ключові моменти:

  • flag.Int, flag.String, flag.Bool, flag.Duration (і т. д.) повертають вказівник: ви використовуєте *n, *upper.
  • flag.Parse() потрібно викликати ОДИН РАЗ, перед читанням значень і перед flag.Args().
  • flag.Args() повертає позиційні аргументи (ті, що знаходяться після -- або після останнього прапора).
  • Прийнятні форми: -n 3, -n=3, --n=3. Логічні значення: -u (істина), -u=false.

Звичайні коди виходу

Go
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)

Оболонки та сценарії очікують цих кодів: використовуйте їх постійно. Ніколи не виходьте з неосновної gor-програми за допомогою os.Exit: вона пропускає кожні defer, включно з скиданням log.

Stderr проти Stdout

Старе і священне правило Unix:

  • os.Stdout → корисний вихід, конвеєрний (cli | grep ...).
  • os.Stderr → діагностика, помилки, індикатори прогресу, використання.
Go
fmt.Println("result: 42")                   // stdout
fmt.Fprintln(os.Stderr, "error: ...")       // stderr

Таким чином cli 2>/dev/null показує лише результати, cli >out.txt перенаправляє лише результат, а помилки залишаються видимими.

log проти fmt

  • fmt для виводу користувача.
  • log для діагностики: додає мітку часу, записує в os.Stderr за замовчуванням, має log.Fatal (журнали + os.Exit(1)) і log.Panic.
Go
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1

Для сучасних програм (Go 1.21+) використовуйте log/slog для структурованого журналювання (JSON або текст, рівні, введені поля).

Архітектура: відокремте main від логіки

Go
func main() {
    if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func run(args []string, stdout, stderr io.Writer) error {
    // here use a new flag.FlagSet instead of the global flag.CommandLine
    fs := flag.NewFlagSet("echo", flag.ContinueOnError)
    fs.SetOutput(stderr)
    n := fs.Int("n", 1, "repetitions")
    if err := fs.Parse(args); err != nil {
        return err
    }
    // ...
    return nil
}

Переваги:

  • Testable: ви передаєте bytes.Buffer як stdout/stderr і перевіряєте вихід.
  • Немає глобального стану: кожен FlagSet ізольований.
  • main залишається маленьким: він готує IO + делегатів.

Підкоманди

Для CLI з підкомандами (mycli serve, mycli migrate), схема без зовнішніх залежностей:

Go
func main() {
    if len(os.Args) < 2 {
        usage()
        os.Exit(2)
    }
    switch os.Args[1] {
    case "serve":
        serveCmd(os.Args[2:])
    case "migrate":
        migrateCmd(os.Args[2:])
    default:
        usage()
        os.Exit(2)
    }
}

Кожна підкоманда має власний flag.NewFlagSet. Для великих проектів такі бібліотеки, як cobra або urfave/cli, структурують цю схему за допомогою автоматично створеної довідки.

вправи

вправи#go.m10.l4.e1
Спроби: 0Завантаження…

Визначення прапора -n типу int зі значенням за замовчуванням 1 і описом 'ripetizioni', показує прапорець.Parse() і значення значення.

Завантаження редактора…

Рішення доступне після 3 спроб

вправи#go.m10.l4.e2
Спроби: 0Завантаження…

Позиційний аргомент не містить прапорця. Проаналізуйте, надрукуйте «аргомент» у Stderr і завершіть через os.Exit(1).

Завантаження редактора…

Рішення доступне після 3 спроб

Quiz#go.m10.l4.e3
Готовий

Який потік повідомлень про помилки та використання в CLI, дружньому до Unix?

Go
fmt.Fprintln(???, "uso: prog ...")
Варіанти відповідей