Pelajaran modul (4/5)
Proyek mini: CLI dengan flag
Mari kita satukan blok penyusun modul sebelumnya untuk membuat CLI idiomatik kecil: penguraian flag dengan paket flag, penanganan argumen posisi, kesalahan pada os.Stderr, dan kode keluar yang benar.
Paket 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)
}
}Poin-poin penting:
flag.Int,flag.String,flag.Bool,flag.Duration(dll.) mengembalikan pointer: Anda menggunakan*n,*upper.flag.Parse()harus dipanggil SEKALI, sebelum membaca nilainya dan sebelumflag.Args().flag.Args()mengembalikan argumen posisional (yang setelah--atau setelah tanda terakhir).- Formulir yang diterima:
-n 3,-n=3,--n=3. Boolean:-u(benar),-u=false.
Kode keluar konvensional
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)Shell dan skrip mengharapkan kode-kode ini: gunakanlah secara konsisten. Jangan pernah keluar dari goroutine non-utama dengan os.Exit: ia akan melewatkan setiap defer, termasuk pembilasan log.
Stderr vs Stdout
Aturan Unix yang lama dan sakral:
os.Stdout→ keluaran berguna, dapat disalurkan (cli | grep ...).os.Stderr→ diagnostik, kesalahan, bilah kemajuan, penggunaan.
fmt.Println("result: 42") // stdout
fmt.Fprintln(os.Stderr, "error: ...") // stderrDengan cara ini cli 2>/dev/null hanya menampilkan hasil, cli >out.txt hanya mengalihkan hasil dan kesalahan tetap terlihat.
log vs fmt
fmtuntuk keluaran pengguna.loguntuk diagnostik: menambahkan stempel waktu, menulis keos.Stderrsecara default, memilikilog.Fatal(log +os.Exit(1)) danlog.Panic.
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1Untuk aplikasi modern (Go 1.21+) gunakan log/slog untuk logging terstruktur (JSON atau teks, level, kolom yang diketik).
Arsitektur: pisahkan main dari logika
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
}Keuntungan:
- Dapat diuji: Anda meneruskan
bytes.Buffersebagai stdout/stderr dan memverifikasi hasilnya. - Tidak ada status global: setiap
FlagSetdiisolasi. maintetap kecil: mempersiapkan delegasi IO+.
Sub-perintah
Untuk CLI dengan subperintah (mycli serve, mycli migrate), skema tanpa ketergantungan eksternal:
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)
}
}Setiap subperintah memiliki flag.NewFlagSet sendiri. Untuk proyek yang lebih besar, perpustakaan seperti cobra atau urfave/cli menyusun skema ini dengan bantuan yang dibuat secara otomatis.
Latihan
Definisikan flag -n jenis ini dengan nilai default 1 dan deskripsi 'ripetizioni', sehingga flag.Parse() dan stempel nilainya.
Solusi tersedia setelah 3 upaya
Jika tidak ada posisi argumen yang sama dengan flag. Parse, cap 'manca argomento' pada Stderr dan akhiri dengan os.Exit(1).
Solusi tersedia setelah 3 upaya
Apakah aliran ini menampilkan pesan kesalahan dan penggunaan di CLI Unix-friendly?
fmt.Fprintln(???, "uso: prog ...")