Go CLI Commands Flags Argument Parsing
Selesai membaca dokumentasi ini?
Kembali ke rujukan utama Go (Golang) untuk melanjutkan topik lainnya.
Kembali ke rujukan utama Go (Golang) untuk melanjutkan topik lainnya.
Membangun CLI (command-line interface) di Go berarti mengubah os.Args — slice string mentah dari shell — menjadi struktur yang bisa dipakai: perintah, opsi, dan argumen. Go punya dukungan bawaan lewat package flag, tapi pola di sekitarnya (subcommand, positional args, validasi) baru bermakna kalau dipahami sebagai satu alur.
git clone, go run. Di Go kita dispatch manual lewat os.Args[1].- atau --, misal -port=8080, --verbose. Diurus flag package.flag.Args().Intinya: flag hanya mengurus flag; positional dan subcommand tetap urusan kita.
Pakai pola ini kalau kamu sedang membuat:
Kalau inputmu cuma satu argumen sederhana dan tidak butuh opsi, cukup baca os.Args langsung tanpa flag. Tapi begitu ada lebih dari satu opsi atau ada subcommand, flag + pola subcommand jauh lebih rapi daripada memecah os.Args manual.
flag mendaftarkan flag ke sebuah flag set, lalu Parse() mengisi nilainya dari argumen.
package main
import (
"flag"
"fmt"
)
func main() {
name := flag.String("name", "world", "nama yang disapa")
count := flag.Int("count", 1, "berapa kali")
verbose := flag.Bool("verbose",
Aturan dasar:
flag.String/Int/Bool/... mengembalikan pointer ke nilai hasil parse.flag.Parse().flag.Parse() mem-parse os.Args[1:].Parse(), sisa argumen yang bukan flag ada di flag.Args().Cara pemanggilan yang didukung (semua setara):
app -name=Budi
app -name Budi
app --name Budi // -- dianggap sama dengan -
Tipe umum yang langsung didukung: String, Int, Int64, Uint, Bool, Float64, Duration.
Untuk nilai majemuk (misal daftar dipisah koma) atau tipe custom, implementasikan interface flag.Value:
type listVar []string
func (l *listVar) String() string { return strings.Join(*l, ",") }
func (l *listVar) Set(
flag.Var dipakai saat tipe tidak punya helper bawaan. Set yang mengakumulasi cocok untuk flag berulang.
Setelah flag.Parse(), apa pun yang tersisa (dan bukan flag) jadi positional.
func main() {
recursive := flag.Bool("r", false, "rekursif")
flag.Parse()
files := flag.Args() // []string positional
if len(files) == 0 {
Pemanggilan:
app -r file1.txt file2.txt
flag.Args() mengembalikan slice, flag.Arg(i) mengembalikan elemen ke-i, flag.NArg() jumlahnya.
CLI seperti app serve -port=8080 butuh dispatch: baca os.Args[1] sebagai nama perintah, lalu berikan sisa argumen ke flag.FlagSet milik perintah itu. Jangan pakai flag global untuk subcommand — tiap subcommand punya flag set sendiri.
package main
import (
"flag"
"fmt"
"os"
)
func main() {
if len(os.Args)
Poin penting:
flag.NewFlagSet(name, handling) membuat set flag independen.handling menentukan reaksi terhadap error: ExitOnError (keluar), ContinueOnError (kembalikan error), PanicOnError.fs.Parse(args), bukan flag.Parse().-h) akan mencetak usage milik FlagSet tersebut.Urutannya selalu begini: parse dulu, validate, baru run. Jangan jalankan efek samping (menulis file, konek DB) sebelum validasi lulus, supaya flag yang salah cuma mencetak pesan dan keluar bersih.
Setelah Parse(), flag sudah memastikan tipe benar (misal -count=abc gagal karena bukan int). Yang tersisa adalah validasi semantik yang flag tidak tahu:
-name="")-dry-run tapi -force)func serveCmd(args []string) {
fs := flag.NewFlagSet("serve", flag.ExitOnError)
port := fs.Int("port", 8080, "port untuk listen")
_ = fs.Parse(args)
Berikut jebakan yang paling sering muncul saat parsing CLI di Go, plus cara menghindarinya.
flag.Parse()Nilai flag tetap default walau user lewatkan opsi.
dir := flag.String("dir", ".", "direktori")
// lupa flag.Parse()
fmt.Println(*dir) // selalu "."Fix: panggil flag.Parse() sekali, setelah semua flag dideklarasikan.
Parse()Karena flag.String mengembalikan pointer, membaca nilainya sebelum parse memberimu default, bukan input user.
name := flag.String("name", "default", "nama")
got := *name // SALAH: dibaca sebelum Parse
flag.Parse()
// got tetap "default" walau user lewat -name=actualFix: simpan pointer-nya, baca *name setelah flag.Parse().
flag berhenti mem-parse begitu menemukan argumen pertama yang bukan flag. Jadi flag di belakang positional tidak diparse.
app file.txt -verbose # -verbose dianggap positional, bukan flag
Fix: minta pengguna menaruh flag di depan, atau validasi bahwa sisa flag.Args() tidak mengandung token berawalan -.
-v false mematikan bool flagUntuk bool flag, bentuk -v menyalakan (true). Bentuk -v false tidak mematikan: false malah jadi argumen positional, dan -v tetap true.
app -v false # v = true, positional = [false]
app -v=false # v = false ✅
Fix: pakai -v=false (bentuk sama-dengan). Atau untuk mati hidup, lebih jelas pakai dua flag enable-x / disable-x atau string "true"/"false".
- vs -- sebagai terminator-flag dan --flag dianggap sama oleh package flag. Jangan kira -- punya makna khusus di setiap posisi.-- (dua tanda minus sendirian) adalah terminator: semua setelahnya jadi positional, meski kelihatan seperti flag.app -- -weird-file.txt # -weird-file.txt jadi positional, aman
- sendiri dianggap argumen biasa (positional), bukan flag.os.Args[0] adalah nama programos.Args[0] berisi path eksekusi, bukan argumen pertama. Argumen asli mulai dari os.Args[1]. Saat membuat FlagSet subcommand, parse os.Args[2:], bukan os.Args[1:].
-h/--helpflag punya help otomatis: -h atau --help mencetak usage lalu keluar (di Go modern exit 0, bukan error). Kalau tampilan default kurang jelas, override flag.Usage:
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "usage: app -name=N [flags]")
flag.PrintDefaults()
}Untuk FlagSet sendiri: fs.Usage = func() { ... }.
Kalau kamu mendaftarkan flag ke set global lalu juga pakai subcommand, flag subcommand bentrok atau ikut ter-parse di level salah.
Fix: tiap subcommand punya flag.NewFlagSet sendiri dan parse argumennya sendiri.
-v untuk "verbose" dan -v untuk "version" di subcommand berbeda tidak masalah kalau masing-masing pakai FlagSet sendiri, tapi di satu set flag, nama pendek harus unik. Dokumentasikan mapping -x → arti agar tidak tabrakan saat tool membesar.
Parse saat ContinueOnErrorKalau pakai ContinueOnError, Parse mengembalikan error (misal flag tidak dikenal, tipe salah) dan tidak keluar sendiri. Lupa mengecek error berarti program jalan dengan nilai salah.
if err := fs.Parse(args); err != nil {
os.Exit(2)
}package main
import (
"flag"
"fmt"
"os"
Pemanggilan:
app greet -name=Budi
app greet -upper Budi
app sum -verbose 1 2 3
Parse(), lalu baru baca nilainya.FlagSet per command dan parse os.Args[2:].flag hanya validasi tipe.flag.Usage / fs.Usage supaya pesan error ramah pengguna.ContinueOnError + cek error bila ingin kontrol keluar sendiri; ExitOnError cocok untuk tool sederhana.os.Args global.flag.Parse() → nilai selalu default*flag sebelum Parse() → dapat default, bukan input-v false tidak mematikan bool; pakai -v=falseos.Args[0] sebagai argumen pertamaParse saat ContinueOnError- dan -- punya makna berbeda (Go anggap sama, kecuali -- sebagai terminator)