Модульні уроки (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)Оболонки та сценарії очікують цих кодів: використовуйте їх постійно. Ніколи не виходьте з неосновної gor-програми за допомогою 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
}Переваги:
- Testable: ви передаєте
bytes.Bufferяк stdout/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 і описом 'ripetizioni', показує прапорець.Parse() і значення значення.
Рішення доступне після 3 спроб
Позиційний аргомент не містить прапорця. Проаналізуйте, надрукуйте «аргомент» у Stderr і завершіть через os.Exit(1).
Рішення доступне після 3 спроб
Який потік повідомлень про помилки та використання в CLI, дружньому до Unix?
fmt.Fprintln(???, "uso: prog ...")