Flag #

Hampir setiap program Go yang berjalan dari command line membutuhkan cara untuk menerima konfigurasi dari pengguna — apakah port yang digunakan, file konfigurasi yang dibaca, atau mode debug yang diaktifkan. Package flag menyediakan parsing argumen command line yang sederhana dan idiomatis: definisikan flag-mu, panggil flag.Parse(), dan nilai sudah tersedia sebagai pointer atau variabel. Package ini sengaja minimalis — tidak mendukung subcommand bertingkat atau format --flag (hanya -flag dan --flag dengan satu sama dua dash), tapi untuk kebutuhan tooling sederhana hingga menengah, ia lebih dari cukup. Untuk CLI yang lebih kompleks, FlagSet memungkinkan implementasi subcommand, dan kombinasi dengan environment variable membuat konfigurasi yang fleksibel.

Gambaran Besar Package flag #

flowchart TD
    F["package flag"] --> Define["Mendefinisikan Flag"]
    F --> Parse["Parsing"]
    F --> Access["Mengakses Nilai"]
    F --> Custom["Custom Type"]
    F --> FlagSet["FlagSet\nuntuk subcommand"]

    Define --> D1["flag.String(name, default, usage)\nreturn *string"]
    Define --> D2["flag.Int(name, default, usage)\nreturn *int"]
    Define --> D3["flag.Bool(name, default, usage)\nreturn *bool"]
    Define --> D4["flag.Duration(name, default, usage)\nreturn *time.Duration"]
    Define --> D5["flag.StringVar(&var, name, default, usage)\nlangsung ke variabel"]

    Parse --> P1["flag.Parse()\nparse os.Args[1:]"]
    Parse --> P2["flag.Args()\nargumen non-flag setelah parse"]
    Parse --> P3["flag.NArg()\njumlah argumen non-flag"]

    Access --> A1["*namaFlag — dereference pointer"]
    Access --> A2["flag.Lookup(name)\ncari flag yang sudah didefinisikan"]

    FlagSet --> FS1["flag.NewFlagSet(name, errorHandling)"]
    FlagSet --> FS2["fs.Parse(args)\nparse slice argumen tertentu"]

    style F fill:#4f86c6,color:#fff
    style Define fill:#e8f5e9
    style Parse fill:#e3f2fd
    style Access fill:#fff3e0
    style FlagSet fill:#f3e5f5

Flag Dasar #

package main

import (
    "flag"
    "fmt"
    "os"
)

func main() {
    // Definisikan flag — kembalikan pointer ke nilai
    host := flag.String("host", "localhost", "host server")
    port := flag.Int("port", 8080, "port server")
    debug := flag.Bool("debug", false, "aktifkan mode debug")
    timeout := flag.Duration("timeout", 30*time.Second, "timeout koneksi")
    config := flag.String("config", "", "path ke file konfigurasi")

    // Alternatif: langsung ke variabel yang sudah ada
    var verbose bool
    flag.BoolVar(&verbose, "verbose", false, "output verbose")

    var maxConn int
    flag.IntVar(&maxConn, "max-conn", 10, "jumlah koneksi maksimal")

    // Parse argumen dari os.Args[1:]
    // HARUS dipanggil setelah semua flag didefinisikan, sebelum digunakan
    flag.Parse()

    // Akses nilai dengan dereference pointer
    fmt.Printf("Host:    %s\n", *host)
    fmt.Printf("Port:    %d\n", *port)
    fmt.Printf("Debug:   %v\n", *debug)
    fmt.Printf("Timeout: %v\n", *timeout)
    fmt.Printf("Verbose: %v\n", verbose)
    fmt.Printf("MaxConn: %d\n", maxConn)

    // Argumen non-flag (setelah semua flag)
    // Contoh: ./app -port 9090 file1.txt file2.txt
    args := flag.Args() // ["file1.txt", "file2.txt"]
    fmt.Printf("Argumen: %v\n", args)
    fmt.Printf("Jumlah argumen: %d\n", flag.NArg())
}

Format Flag yang Didukung #

# Format yang valid — semuanya setara
./app -host localhost
./app --host localhost
./app -host=localhost
./app --host=localhost

# Boolean flag — keduanya setara
./app -debug
./app -debug=true
./app -debug=false

# Urutan tidak penting untuk flag
./app -port 9090 -host example.com -debug

# Argumen non-flag di akhir
./app -port 9090 file1.txt file2.txt
# flag.Args() = ["file1.txt", "file2.txt"]

# -- menghentikan parsing flag
./app -port 9090 -- -ini-bukan-flag.txt
# flag.Args() = ["-ini-bukan-flag.txt"]

Usage — Pesan Bantuan #

func main() {
    // Kustomisasi pesan usage
    flag.Usage = func() {
        fmt.Fprintf(os.Stderr, "Penggunaan: %s [flags] [file...]\n\n", os.Args[0])
        fmt.Fprintf(os.Stderr, "Server HTTP sederhana.\n\n")
        fmt.Fprintf(os.Stderr, "Flag:\n")
        flag.PrintDefaults() // cetak semua flag dengan format standar
        fmt.Fprintf(os.Stderr, "\nContoh:\n")
        fmt.Fprintf(os.Stderr, "  %s -port 9090 -debug\n", os.Args[0])
        fmt.Fprintf(os.Stderr, "  %s -config /etc/myapp/config.yaml\n", os.Args[0])
    }

    host := flag.String("host", "0.0.0.0", "alamat `host` yang di-listen")
    port := flag.Int("port", 8080, "nomor `port` (1-65535)")

    flag.Parse()
    _ = host
    _ = port
}

// Output dari -help atau -h:
// Penggunaan: ./myapp [flags] [file...]
//
// Server HTTP sederhana.
//
// Flag:
//   -host host
//     	alamat host yang di-listen (default "0.0.0.0")
//   -port port
//     	nomor port (1-65535) (default 8080)
//
// Contoh:
//   ./myapp -port 9090 -debug
//   ./myapp -config /etc/myapp/config.yaml

Custom Flag Type #

Untuk tipe yang tidak didukung secara bawaan (selain string, int, bool, float64, duration), implementasikan interface flag.Value:

// flag.Value interface:
// type Value interface {
//     String() string  — nilai default dan tampilan saat -help
//     Set(string) error — set nilai dari string argumen
// }

// Contoh 1: flag untuk slice string (nilai berulang)
type StringSlice []string

func (ss *StringSlice) String() string {
    return strings.Join(*ss, ", ")
}

func (ss *StringSlice) Set(s string) error {
    *ss = append(*ss, s)
    return nil
}

// Penggunaan:
// ./app -tag backend -tag api -tag v2
// tags akan berisi ["backend", "api", "v2"]

// Contoh 2: flag untuk level log
type LogLevel int

const (
    LevelDebug LogLevel = iota
    LevelInfo
    LevelWarn
    LevelError
)

func (l *LogLevel) String() string {
    switch *l {
    case LevelDebug:
        return "debug"
    case LevelInfo:
        return "info"
    case LevelWarn:
        return "warn"
    case LevelError:
        return "error"
    default:
        return "unknown"
    }
}

func (l *LogLevel) Set(s string) error {
    switch strings.ToLower(s) {
    case "debug":
        *l = LevelDebug
    case "info":
        *l = LevelInfo
    case "warn", "warning":
        *l = LevelWarn
    case "error":
        *l = LevelError
    default:
        return fmt.Errorf("level tidak valid: %q (valid: debug, info, warn, error)", s)
    }
    return nil
}

// Mendaftarkan custom flag
func main() {
    var tags StringSlice
    flag.Var(&tags, "tag", "tag untuk filter (bisa diulang)")

    var logLevel LogLevel = LevelInfo // default Info
    flag.Var(&logLevel, "log-level", "level logging (debug|info|warn|error)")

    flag.Parse()

    fmt.Println("Tags:", tags)
    fmt.Println("Log level:", logLevel)
}

FlagSet — Subcommand #

flag.FlagSet memungkinkan definisi flag per subcommand — setiap subcommand punya set flag-nya sendiri yang terpisah:

flowchart TD
    App["./myapp"] --> Args["os.Args"]
    Args --> Sub{subcommand?}

    Sub -- "serve" --> ServFS["serveFlagSet\n  -port int\n  -host string\n  -tls bool"]
    Sub -- "migrate" --> MigrFS["migrateFlagSet\n  -db string\n  -dry-run bool\n  -steps int"]
    Sub -- "export" --> ExpFS["exportFlagSet\n  -format string\n  -output string\n  -compress bool"]
    Sub -- "help / lainnya" --> Help["Tampilkan usage"]

    ServFS --> ServeCmd["jalankanServer()"]
    MigrFS --> MigrCmd["jalankanMigrasi()"]
    ExpFS --> ExpCmd["jalankanEkspor()"]

    style App fill:#4f86c6,color:#fff
    style ServFS fill:#e8f5e9
    style MigrFS fill:#e3f2fd
    style ExpFS fill:#fff3e0
func main() {
    // Periksa subcommand
    if len(os.Args) < 2 {
        fmt.Fprintf(os.Stderr, "Penggunaan: %s <subcommand> [flags]\n\n", os.Args[0])
        fmt.Fprintf(os.Stderr, "Subcommand yang tersedia:\n")
        fmt.Fprintf(os.Stderr, "  serve    — jalankan HTTP server\n")
        fmt.Fprintf(os.Stderr, "  migrate  — jalankan migrasi database\n")
        fmt.Fprintf(os.Stderr, "  export   — ekspor data\n")
        os.Exit(1)
    }

    switch os.Args[1] {
    case "serve":
        jalankanServe(os.Args[2:])
    case "migrate":
        jalankanMigrasi(os.Args[2:])
    case "export":
        jalankanEkspor(os.Args[2:])
    case "help", "-help", "--help", "-h":
        // Tampilkan help umum
        fmt.Println("Gunakan: myapp <subcommand> -help untuk bantuan")
    default:
        fmt.Fprintf(os.Stderr, "Subcommand tidak dikenal: %q\n", os.Args[1])
        os.Exit(1)
    }
}

func jalankanServe(args []string) {
    // FlagSet untuk subcommand serve
    fs := flag.NewFlagSet("serve", flag.ExitOnError)

    host := fs.String("host", "0.0.0.0", "alamat host")
    port := fs.Int("port", 8080, "nomor port")
    tls := fs.Bool("tls", false, "aktifkan TLS")
    certFile := fs.String("cert", "", "path ke sertifikat TLS")
    keyFile := fs.String("key", "", "path ke private key TLS")

    // Kustomisasi usage untuk subcommand ini
    fs.Usage = func() {
        fmt.Fprintf(os.Stderr, "Penggunaan: myapp serve [flags]\n\nFlag:\n")
        fs.PrintDefaults()
    }

    // Parse args untuk subcommand ini saja
    if err := fs.Parse(args); err != nil {
        os.Exit(1)
    }

    // Validasi
    if *tls && (*certFile == "" || *keyFile == "") {
        fmt.Fprintf(os.Stderr, "Error: -cert dan -key wajib jika -tls diaktifkan\n")
        os.Exit(1)
    }

    fmt.Printf("Menjalankan server di %s:%d (TLS: %v)\n", *host, *port, *tls)
}

func jalankanMigrasi(args []string) {
    fs := flag.NewFlagSet("migrate", flag.ExitOnError)

    dsn := fs.String("db", os.Getenv("DATABASE_URL"), "DSN database")
    dryRun := fs.Bool("dry-run", false, "tampilkan SQL tanpa eksekusi")
    steps := fs.Int("steps", 0, "jumlah migrasi (0 = semua)")

    fs.Usage = func() {
        fmt.Fprintf(os.Stderr, "Penggunaan: myapp migrate [flags]\n\nFlag:\n")
        fs.PrintDefaults()
    }

    fs.Parse(args)

    if *dsn == "" {
        fmt.Fprintf(os.Stderr, "Error: -db atau DATABASE_URL wajib diisi\n")
        os.Exit(1)
    }

    fmt.Printf("Migrasi database: %s (dry-run: %v, steps: %d)\n",
        *dsn, *dryRun, *steps)
}

func jalankanEkspor(args []string) {
    fs := flag.NewFlagSet("export", flag.ExitOnError)

    format := fs.String("format", "json", "format output (json|csv|xlsx)")
    output := fs.String("output", "-", "file output (- untuk stdout)")
    compress := fs.Bool("compress", false, "kompres output dengan gzip")

    fs.Parse(args)

    fmt.Printf("Ekspor: format=%s, output=%s, compress=%v\n",
        *format, *output, *compress)
}

Integrasi dengan Environment Variable #

Pattern yang sangat umum di aplikasi produksi adalah mendukung konfigurasi dari keduanya — flag CLI untuk penggunaan interaktif, environment variable untuk deployment otomatis:

flowchart LR
    subgraph Prioritas["Prioritas Konfigurasi (tinggi ke rendah)"]
        P1["1. Flag CLI\n(-port 9090)"]
        P2["2. Environment Variable\n(PORT=9090)"]
        P3["3. File Konfigurasi\n(config.yaml)"]
        P4["4. Default Value\n(port: 8080)"]
    end

    P1 --> P2 --> P3 --> P4

    style P1 fill:#e8f5e9
    style P2 fill:#e3f2fd
    style P3 fill:#fff3e0
    style P4 fill:#f3e5f5
// Helper: ambil dari env atau gunakan default
func envOr(key, defaultVal string) string {
    if val := os.Getenv(key); val != "" {
        return val
    }
    return defaultVal
}

func envOrInt(key string, defaultVal int) int {
    if val := os.Getenv(key); val != "" {
        n, err := strconv.Atoi(val)
        if err == nil {
            return n
        }
    }
    return defaultVal
}

func envOrBool(key string, defaultVal bool) bool {
    if val := os.Getenv(key); val != "" {
        b, err := strconv.ParseBool(val)
        if err == nil {
            return b
        }
    }
    return defaultVal
}

// Konfigurasi yang mendukung keduanya
func main() {
    // Default diambil dari env, tapi bisa di-override dengan flag
    host := flag.String("host",
        envOr("APP_HOST", "0.0.0.0"),
        "host server (env: APP_HOST)")

    port := flag.Int("port",
        envOrInt("PORT", 8080),
        "port server (env: PORT)")

    debug := flag.Bool("debug",
        envOrBool("APP_DEBUG", false),
        "mode debug (env: APP_DEBUG)")

    dbURL := flag.String("db",
        envOr("DATABASE_URL", ""),
        "URL database (env: DATABASE_URL)")

    flag.Parse()

    // Validasi nilai wajib
    if *dbURL == "" {
        fmt.Fprintf(os.Stderr,
            "Error: -db atau DATABASE_URL wajib diisi\n")
        flag.Usage()
        os.Exit(1)
    }

    fmt.Printf("Konfigurasi:\n")
    fmt.Printf("  Host:  %s\n", *host)
    fmt.Printf("  Port:  %d\n", *port)
    fmt.Printf("  Debug: %v\n", *debug)
    fmt.Printf("  DB:    %s\n", maskDSN(*dbURL))
}

func maskDSN(dsn string) string {
    // Sembunyikan password dari DSN untuk logging
    re := regexp.MustCompile(`://[^:]+:[^@]+@`)
    return re.ReplaceAllString(dsn, "://***:***@")
}

ErrorHandling di FlagSet #

// flag.ContinueOnError — kembalikan error, jangan panic/exit
fs := flag.NewFlagSet("myapp", flag.ContinueOnError)

// flag.ExitOnError — panggil os.Exit(2) saat error (default untuk flag package)
fs2 := flag.NewFlagSet("myapp", flag.ExitOnError)

// flag.PanicOnError — panic saat error
fs3 := flag.NewFlagSet("myapp", flag.PanicOnError)

// Contoh penggunaan ContinueOnError untuk kontrol penuh
fs4 := flag.NewFlagSet("myapp", flag.ContinueOnError)
var buf bytes.Buffer
fs4.SetOutput(&buf) // redirect output error ke buffer

port := fs4.Int("port", 8080, "port server")

if err := fs4.Parse(os.Args[1:]); err != nil {
    if err == flag.ErrHelp {
        // Pengguna meminta -help
        fmt.Println(buf.String())
        os.Exit(0)
    }
    fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n%s", err, buf.String())
    os.Exit(1)
}

_ = port

Pola Penggunaan di Produksi #

Config Struct dari Flag #

// Kumpulkan semua konfigurasi dalam satu struct
type AppConfig struct {
    Host       string
    Port       int
    Debug      bool
    DatabaseURL string
    LogLevel   string
    MaxWorkers int
    Timeout    time.Duration
    Tags       []string
}

func parseConfig() *AppConfig {
    cfg := &AppConfig{}

    // Flag dengan nilai default dari environment
    flag.StringVar(&cfg.Host, "host",
        envOr("HOST", "0.0.0.0"), "alamat host (env: HOST)")
    flag.IntVar(&cfg.Port, "port",
        envOrInt("PORT", 8080), "port server (env: PORT)")
    flag.BoolVar(&cfg.Debug, "debug",
        envOrBool("DEBUG", false), "mode debug (env: DEBUG)")
    flag.StringVar(&cfg.DatabaseURL, "db",
        envOr("DATABASE_URL", ""), "URL database (env: DATABASE_URL)")
    flag.StringVar(&cfg.LogLevel, "log-level",
        envOr("LOG_LEVEL", "info"), "level log (env: LOG_LEVEL)")
    flag.IntVar(&cfg.MaxWorkers, "workers",
        envOrInt("MAX_WORKERS", 4), "jumlah worker (env: MAX_WORKERS)")
    flag.DurationVar(&cfg.Timeout, "timeout",
        30*time.Second, "timeout operasi")

    // Custom flag untuk slice
    var tags StringSlice
    flag.Var(&tags, "tag", "tag filter (bisa diulang)")

    flag.Parse()

    cfg.Tags = []string(tags)
    return cfg
}

func validasiConfig(cfg *AppConfig) error {
    var errs []string

    if cfg.DatabaseURL == "" {
        errs = append(errs, "database URL wajib diisi (-db atau DATABASE_URL)")
    }
    if cfg.Port < 1 || cfg.Port > 65535 {
        errs = append(errs, fmt.Sprintf("port harus 1-65535, dapat: %d", cfg.Port))
    }
    validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
    if !validLevels[cfg.LogLevel] {
        errs = append(errs, fmt.Sprintf("log-level tidak valid: %q", cfg.LogLevel))
    }

    if len(errs) > 0 {
        return fmt.Errorf("konfigurasi tidak valid:\n  - %s",
            strings.Join(errs, "\n  - "))
    }
    return nil
}

func main() {
    cfg := parseConfig()

    if err := validasiConfig(cfg); err != nil {
        fmt.Fprintln(os.Stderr, err)
        fmt.Fprintf(os.Stderr, "\nGunakan -help untuk melihat semua flag yang tersedia.\n")
        os.Exit(1)
    }

    fmt.Printf("Menjalankan aplikasi dengan konfigurasi:\n")
    fmt.Printf("  Host:     %s:%d\n", cfg.Host, cfg.Port)
    fmt.Printf("  Debug:    %v\n", cfg.Debug)
    fmt.Printf("  Workers:  %d\n", cfg.MaxWorkers)
    fmt.Printf("  Timeout:  %v\n", cfg.Timeout)
    fmt.Printf("  Log:      %s\n", cfg.LogLevel)
}

CLI Tool Lengkap #

// Tool untuk manipulasi file — contoh CLI yang lengkap
func main() {
    // Global flag
    verbose := flag.Bool("v", false, "output verbose")
    version := flag.Bool("version", false, "tampilkan versi")

    flag.Usage = func() {
        fmt.Fprintf(os.Stderr, `Penggunaan: filetool [flags] <subcommand> [subcommand-flags] [args]

Subcommand:
  copy    — salin file
  move    — pindahkan file
  hash    — hitung hash file
  search  — cari file

Flag global:
`)
        flag.PrintDefaults()
        fmt.Fprintf(os.Stderr, "\nGunakan 'filetool <subcommand> -help' untuk bantuan subcommand.\n")
    }

    // Parsing dengan -v dan -version harus sebelum subcommand
    // Tapi stop sebelum subcommand untuk tidak memblokir flag-nya
    flag.Parse()

    if *version {
        fmt.Println("filetool v1.0.0")
        os.Exit(0)
    }

    args := flag.Args()
    if len(args) == 0 {
        flag.Usage()
        os.Exit(1)
    }

    subcommand := args[0]
    subArgs := args[1:]

    switch subcommand {
    case "copy":
        subCopy(*verbose, subArgs)
    case "move":
        subMove(*verbose, subArgs)
    case "hash":
        subHash(*verbose, subArgs)
    case "search":
        subSearch(*verbose, subArgs)
    default:
        fmt.Fprintf(os.Stderr, "Error: subcommand tidak dikenal: %q\n\n", subcommand)
        flag.Usage()
        os.Exit(1)
    }
}

func subHash(verbose bool, args []string) {
    fs := flag.NewFlagSet("hash", flag.ExitOnError)
    algo := fs.String("algo", "sha256", "algoritma hash (sha256|sha512|md5)")
    fs.Usage = func() {
        fmt.Fprintf(os.Stderr, "Penggunaan: filetool hash [flags] <file...>\n\nFlag:\n")
        fs.PrintDefaults()
    }
    fs.Parse(args)

    files := fs.Args()
    if len(files) == 0 {
        fmt.Fprintln(os.Stderr, "Error: minimal satu file diperlukan")
        fs.Usage()
        os.Exit(1)
    }

    for _, f := range files {
        hash, err := hitungHashFile(f, *algo)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Error: %v\n", err)
            continue
        }
        if verbose {
            fmt.Printf("%s (%s): %s\n", f, *algo, hash)
        } else {
            fmt.Printf("%s  %s\n", hash, f)
        }
    }
}

Kapan Beralih ke Alternatif #

Tetap gunakan flag jika:
  ✓ Tools sederhana dengan sedikit flag
  ✓ Zero external dependency
  ✓ Flag tanpa subcommand bertingkat
  ✓ Ingin sesuatu yang idiomatis dan mudah dipahami

Pertimbangkan library eksternal jika:
  ✗ Subcommand bertingkat (app server start --port 8080)
    → cobra (github.com/spf13/cobra) — paling populer
    → urfave/cli — alternatif yang lebih sederhana
  ✗ Auto-generate bash/zsh completion
    → cobra mendukung ini built-in
  ✗ Validasi dan transformasi flag yang kompleks
    → kong (github.com/alecthomas/kong)
  ✗ Konfigurasi dari file + env + flag dalam satu package
    → viper (github.com/spf13/viper) — sering dipakai dengan cobra
  ✗ POSIX-style flag dengan shorthand (-v, --verbose)
    → pflag (github.com/spf13/pflag) — dipakai oleh cobra

Catatan: flag package mendukung baik -flag maupun --flag,
tapi tidak mendukung penggabungan shorthand seperti -vdf (= -v -d -f)

Ringkasan #

  • flag.Parse() harus dipanggil setelah semua flag didefinisikan dan sebelum nilai flag diakses — biasanya di awal main().
  • flag.StringVar, flag.IntVar, dll. untuk mengisi langsung ke variabel yang sudah ada — lebih bersih dari menyimpan pointer hasil flag.String() dll.
  • flag.Usage bisa di-override dengan fungsi custom untuk pesan bantuan yang lebih informatif dan branded.
  • flag.FlagSet untuk subcommand — setiap subcommand punya set flag-nya sendiri yang di-parse dari os.Args[2:] (atau slice yang relevan).
  • Kombinasikan dengan environment variable menggunakan pola envOr — flag CLI bisa override env, tapi env menyediakan default yang lebih fleksibel dari hardcoded value.
  • Custom flag.Value untuk tipe yang tidak didukung bawaan — implementasikan String() dan Set(string) error untuk tipe apapun seperti slice, enum, atau custom struct.
  • Validasi setelah flag.Parse() — periksa nilai flag yang diparsing untuk memastikan kombinasi flag yang valid (misalnya: -tls tanpa -cert dan -key).
  • flag.Args() untuk mengakses argumen non-flag setelah parsing — berguna untuk posisional arguments seperti nama file.
  • flag.ErrHelp saat menggunakan ContinueOnError — deteksi permintaan -help dan tangani secara eksplisit untuk kontrol penuh atas output.

← Sebelumnya: Testing   Berikutnya: Sync Atomic →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact