Перейти к основному содержимому
Модуль 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)

Оболочки и сценарии ожидают эти коды: используйте их последовательно. Никогда не выходите из неосновной горутины с помощью 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
}

Преимущества:

  • Тестируемый: вы передаете bytes.Buffer как стандартный вывод/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 и описанием «соблюдения», выберите flag.Parse() и укажите значение.

Загрузка редактора…

Решение доступно после 3 попыток

Упражнение#go.m10.l4.e2
Попыток: 0Загрузка…

Если нет другого аргумента, позиционного допо flag.Parse, штамп 'manca argomento' на Stderr и конец с os.Exit(1).

Загрузка редактора…

Решение доступно после 3 попыток

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

Есть ли поток сообщений об ошибках и использование в CLI, дружественном к Unix?

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