Уроки модуля (4/5)
Мини-проект: CLI с флагами
Давайте объединим строительные блоки предыдущих модулей, чтобы построить небольшой идиоматический CLI: анализ флагов с помощью пакета flag, обработка позиционных аргументов, ошибок в os.Stderr и правильный код выхода.
Пакет flag
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.
Обычные коды выхода
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→ диагностика, ошибки, индикаторы выполнения, использование.
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.
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1Для современных приложений (Go 1.21+) используйте log/slog для структурированного ведения журналов (JSON или текст, уровни, введенные поля).
Архитектура: отделить main от логики
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) схема без внешних зависимостей:
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, структурируют эту схему с помощью автоматически создаваемой справки.
Упражнения
Определите флаг -n в типе int со значением по умолчанию 1 и описанием «соблюдения», выберите flag.Parse() и укажите значение.
Решение доступно после 3 попыток
Если нет другого аргумента, позиционного допо flag.Parse, штамп 'manca argomento' на Stderr и конец с os.Exit(1).
Решение доступно после 3 попыток
Есть ли поток сообщений об ошибках и использование в CLI, дружественном к Unix?
fmt.Fprintln(???, "uso: prog ...")