17 minute read

개요

Go의 flag 패키지는 POSIX 스타일의 커맨드 라인 플래그를 파싱하는 표준 라이브러리입니다.

주요 특징:

  • 표준 라이브러리: 외부 의존성 없음
  • 타입 안전: 다양한 기본 타입 지원
  • 자동 도움말: -h, --help 자동 생성
  • FlagSet: 서브커맨드 구현 가능
  • 커스텀 플래그: Value 인터페이스로 확장
  • POSIX 스타일: -flag, --flag, -flag=value
  • 단순성: 간단한 CLI 도구에 최적

기본 사용법

1. 플래그 정의 방법

package main

import (
    "flag"
    "fmt"
)

func main() {
    // 방법 1: 포인터 반환
    namePtr := flag.String("name", "Guest", "your name")
    agePtr := flag.Int("age", 0, "your age")
    verbosePtr := flag.Bool("verbose", false, "verbose output")
    
    // 방법 2: 변수에 바인딩 (권장)
    var name string
    var age int
    var verbose bool
    
    flag.StringVar(&name, "name", "Guest", "your name")
    flag.IntVar(&age, "age", 0, "your age")
    flag.BoolVar(&verbose, "verbose", false, "verbose output")
    
    // 플래그 파싱
    flag.Parse()
    
    // 방법 1: 포인터 역참조
    fmt.Println("Name (ptr):", *namePtr)
    
    // 방법 2: 변수 직접 사용
    fmt.Println("Name (var):", name)
    fmt.Println("Age:", age)
    fmt.Println("Verbose:", verbose)
    
    // 남은 인자들 (non-flag arguments)
    fmt.Println("Remaining args:", flag.Args())
    fmt.Println("Number of args:", flag.NArg())
}

실행 예제:

$ go run main.go -name=John -age=30 -verbose file1.txt file2.txt
Name (ptr): John
Name (var): John
Age: 30
Verbose: true
Remaining args: [file1.txt file2.txt]
Number of args: 2

$ go run main.go -name John -age 30
Name (var): John
Age: 30
Verbose: false
Remaining args: []

$ go run main.go --help
Usage of main:
  -age int
        your age
  -name string
        your name (default "Guest")
  -verbose
        verbose output

2. 지원되는 플래그 형식

# 다음은 모두 동일
-name=value
-name value
--name=value  # 이중 하이픈도 허용
--name value

# 불린 플래그
-verbose      # true
-verbose=true
-verbose=false
-verbose true # 주의: "true"는 다음 인자로 간주될 수 있음

# 여러 플래그
-name=John -age=30 -verbose
-name John -age 30 -verbose

3. 플래그 타입

func main() {
    // String
    str := flag.String("string", "default", "string value")
    
    // Integer types
    intVal := flag.Int("int", 0, "int value")
    int64Val := flag.Int64("int64", 0, "int64 value")
    uintVal := flag.Uint("uint", 0, "uint value")
    uint64Val := flag.Uint64("uint64", 0, "uint64 value")
    
    // Float
    float64Val := flag.Float64("float", 0.0, "float64 value")
    
    // Boolean
    boolVal := flag.Bool("bool", false, "bool value")
    
    // Duration
    durationVal := flag.Duration("duration", 0, "time.Duration value")
    
    flag.Parse()
    
    fmt.Printf("String: %s\n", *str)
    fmt.Printf("Int: %d\n", *intVal)
    fmt.Printf("Float: %.2f\n", *float64Val)
    fmt.Printf("Bool: %v\n", *boolVal)
    fmt.Printf("Duration: %v\n", *durationVal)
}

실행:

$ go run main.go -string=hello -int=42 -float=3.14 -bool -duration=5s
String: hello
Int: 42
Float: 3.14
Bool: true
Duration: 5s

고급 기능

1. 플래그 존재 여부 확인

func main() {
    var name string
    var age int
    
    flag.StringVar(&name, "name", "", "name")
    flag.IntVar(&age, "age", 0, "age")
    
    flag.Parse()
    
    // 플래그가 실제로 설정되었는지 확인
    nameSet := false
    ageSet := false
    
    flag.Visit(func(f *flag.Flag) {
        if f.Name == "name" {
            nameSet = true
        }
        if f.Name == "age" {
            ageSet = true
        }
    })
    
    if nameSet {
        fmt.Println("Name was explicitly set to:", name)
    } else {
        fmt.Println("Name was not set (using default)")
    }
    
    if ageSet {
        fmt.Println("Age was explicitly set to:", age)
    } else {
        fmt.Println("Age was not set (using default)")
    }
}

실행:

$ go run main.go -name=John
Name was explicitly set to: John
Age was not set (using default)

$ go run main.go -name=John -age=0
Name was explicitly set to: John
Age was explicitly set to: 0

2. 모든 플래그 순회

func main() {
    flag.String("name", "Guest", "name")
    flag.Int("age", 0, "age")
    flag.Bool("verbose", false, "verbose")
    
    flag.Parse()
    
    fmt.Println("=== All defined flags ===")
    flag.VisitAll(func(f *flag.Flag) {
        fmt.Printf("-%s: %v (default: %q)\n", f.Name, f.Value, f.DefValue)
    })
    
    fmt.Println("\n=== Flags that were set ===")
    flag.Visit(func(f *flag.Flag) {
        fmt.Printf("-%s: %v\n", f.Name, f.Value)
    })
}

실행:

$ go run main.go -name=John -verbose
=== All defined flags ===
-age: 0 (default: "0")
-name: John (default: "Guest")
-verbose: true (default: "false")

=== Flags that were set ===
-name: John
-verbose: true

3. 플래그 값 조회

func main() {
    flag.String("name", "Guest", "name")
    flag.Int("port", 8080, "port")
    
    flag.Parse()
    
    // Lookup으로 플래그 값 가져오기
    if nameFlag := flag.Lookup("name"); nameFlag != nil {
        fmt.Println("Name value:", nameFlag.Value.String())
        fmt.Println("Name default:", nameFlag.DefValue)
    }
    
    if portFlag := flag.Lookup("port"); portFlag != nil {
        fmt.Println("Port value:", portFlag.Value.String())
        fmt.Println("Port default:", portFlag.DefValue)
    }
    
    // 존재하지 않는 플래그
    if missingFlag := flag.Lookup("missing"); missingFlag == nil {
        fmt.Println("Flag 'missing' does not exist")
    }
}

4. 플래그 동적 설정

func main() {
    flag.String("name", "Guest", "name")
    
    flag.Parse()
    
    // 파싱 후 플래그 값 변경
    if nameFlag := flag.Lookup("name"); nameFlag != nil {
        nameFlag.Value.Set("Modified")
        fmt.Println("Modified name:", nameFlag.Value.String())
    }
}

FlagSet

1. 기본 FlagSet

func main() {
    // 새 FlagSet 생성
    fs := flag.NewFlagSet("myapp", flag.ExitOnError)
    
    var name string
    var verbose bool
    
    fs.StringVar(&name, "name", "Guest", "your name")
    fs.BoolVar(&verbose, "verbose", false, "verbose output")
    
    // FlagSet 파싱
    fs.Parse(os.Args[1:])
    
    fmt.Printf("Name: %s, Verbose: %v\n", name, verbose)
    fmt.Println("Remaining args:", fs.Args())
}

2. 에러 처리 모드

func main() {
    // ExitOnError: 에러 시 os.Exit(2) 호출 (기본)
    fs1 := flag.NewFlagSet("app1", flag.ExitOnError)
    
    // ContinueOnError: 에러를 반환
    fs2 := flag.NewFlagSet("app2", flag.ContinueOnError)
    
    // PanicOnError: 에러 시 패닉
    fs3 := flag.NewFlagSet("app3", flag.PanicOnError)
    
    // ContinueOnError 예제
    fs2.String("name", "", "name")
    
    if err := fs2.Parse([]string{"-invalid"}); err != nil {
        fmt.Println("Parse error:", err)
        // 에러 처리 후 계속 실행 가능
    }
}

3. 서브커맨드 구현

package main

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

func main() {
    // 서브커맨드별 FlagSet
    addCmd := flag.NewFlagSet("add", flag.ExitOnError)
    addName := addCmd.String("name", "", "item name")
    addTags := addCmd.String("tags", "", "comma-separated tags")
    
    listCmd := flag.NewFlagSet("list", flag.ExitOnError)
    listVerbose := listCmd.Bool("verbose", false, "verbose listing")
    listFilter := listCmd.String("filter", "", "filter pattern")
    
    deleteCmd := flag.NewFlagSet("delete", flag.ExitOnError)
    deleteForce := deleteCmd.Bool("force", false, "force deletion")
    deleteAll := deleteCmd.Bool("all", false, "delete all")
    
    // 서브커맨드 확인
    if len(os.Args) < 2 {
        fmt.Println("expected 'add', 'list', or 'delete' subcommands")
        os.Exit(1)
    }
    
    switch os.Args[1] {
    case "add":
        addCmd.Parse(os.Args[2:])
        fmt.Printf("Adding item: %s with tags: %s\n", *addName, *addTags)
        fmt.Println("Remaining args:", addCmd.Args())
        
    case "list":
        listCmd.Parse(os.Args[2:])
        fmt.Printf("Listing items (verbose: %v, filter: %s)\n", 
            *listVerbose, *listFilter)
        
    case "delete":
        deleteCmd.Parse(os.Args[2:])
        if *deleteAll {
            fmt.Println("Deleting all items")
        } else if len(deleteCmd.Args()) > 0 {
            fmt.Printf("Deleting: %v (force: %v)\n", 
                deleteCmd.Args(), *deleteForce)
        } else {
            fmt.Println("No items specified for deletion")
        }
        
    default:
        fmt.Printf("Unknown subcommand: %s\n", os.Args[1])
        os.Exit(1)
    }
}

실행:

$ go run main.go add -name=item1 -tags=go,cli
Adding item: item1 with tags: go,cli
Remaining args: []

$ go run main.go list -verbose -filter="*.go"
Listing items (verbose: true, filter: *.go)

$ go run main.go delete -force item1 item2
Deleting: [item1 item2] (force: true)

$ go run main.go delete -all
Deleting all items

4. 도움말 커스터마이징

func main() {
    fs := flag.NewFlagSet("myapp", flag.ExitOnError)
    
    var name string
    var port int
    
    fs.StringVar(&name, "name", "app", "application name")
    fs.IntVar(&port, "port", 8080, "server port")
    
    // 커스텀 Usage 함수
    fs.Usage = func() {
        fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] <command>\n\n", os.Args[0])
        fmt.Fprintf(os.Stderr, "A simple application manager\n\n")
        fmt.Fprintf(os.Stderr, "OPTIONS:\n")
        fs.PrintDefaults()
        fmt.Fprintf(os.Stderr, "\nCOMMANDS:\n")
        fmt.Fprintf(os.Stderr, "  start    Start the application\n")
        fmt.Fprintf(os.Stderr, "  stop     Stop the application\n")
        fmt.Fprintf(os.Stderr, "  status   Check application status\n")
        fmt.Fprintf(os.Stderr, "\nEXAMPLES:\n")
        fmt.Fprintf(os.Stderr, "  %s -name=myapp -port=9000 start\n", os.Args[0])
        fmt.Fprintf(os.Stderr, "  %s status\n", os.Args[0])
    }
    
    fs.Parse(os.Args[1:])
    
    if fs.NArg() < 1 {
        fs.Usage()
        os.Exit(1)
    }
    
    command := fs.Arg(0)
    fmt.Printf("Executing '%s' with name=%s, port=%d\n", command, name, port)
}

커스텀 플래그 타입

1. Value 인터페이스

type Value interface {
    String() string
    Set(string) error
}

2. 문자열 슬라이스 플래그

type StringSlice []string

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

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

func main() {
    var tags StringSlice
    
    flag.Var(&tags, "tag", "tag to add (can be specified multiple times)")
    flag.Parse()
    
    fmt.Println("Tags:", tags)
    fmt.Printf("Tags array: %v\n", []string(tags))
}

실행:

$ go run main.go -tag=golang -tag=cli -tag=tutorial
Tags: golang,cli,tutorial
Tags array: [golang cli tutorial]

3. URL 플래그

import "net/url"

type URLValue struct {
    URL *url.URL
}

func (u *URLValue) String() string {
    if u.URL != nil {
        return u.URL.String()
    }
    return ""
}

func (u *URLValue) Set(value string) error {
    parsedURL, err := url.Parse(value)
    if err != nil {
        return fmt.Errorf("invalid URL: %v", err)
    }
    u.URL = parsedURL
    return nil
}

func main() {
    var apiURL URLValue
    
    flag.Var(&apiURL, "api", "API endpoint URL")
    flag.Parse()
    
    if apiURL.URL != nil {
        fmt.Println("API URL:", apiURL.URL.String())
        fmt.Println("Scheme:", apiURL.URL.Scheme)
        fmt.Println("Host:", apiURL.URL.Host)
        fmt.Println("Path:", apiURL.URL.Path)
    }
}

실행:

$ go run main.go -api=https://api.example.com/v1/users
API URL: https://api.example.com/v1/users
Scheme: https
Host: api.example.com
Path: /v1/users

4. 열거형 플래그

type LogLevel string

const (
    LogLevelDebug LogLevel = "debug"
    LogLevelInfo  LogLevel = "info"
    LogLevelWarn  LogLevel = "warn"
    LogLevelError LogLevel = "error"
)

func (l *LogLevel) String() string {
    return string(*l)
}

func (l *LogLevel) Set(value string) error {
    switch value {
    case "debug", "info", "warn", "error":
        *l = LogLevel(value)
        return nil
    default:
        return fmt.Errorf("invalid log level: %s (must be debug, info, warn, or error)", value)
    }
}

func main() {
    var logLevel LogLevel = LogLevelInfo
    
    flag.Var(&logLevel, "log-level", "log level (debug|info|warn|error)")
    flag.Parse()
    
    fmt.Println("Log level:", logLevel)
}

실행:

$ go run main.go -log-level=debug
Log level: debug

$ go run main.go -log-level=invalid
invalid value "invalid" for flag -log-level: invalid log level: invalid (must be debug, info, warn, or error)

5. 정규표현식 플래그

import "regexp"

type RegexpValue struct {
    Regexp *regexp.Regexp
}

func (r *RegexpValue) String() string {
    if r.Regexp != nil {
        return r.Regexp.String()
    }
    return ""
}

func (r *RegexpValue) Set(value string) error {
    re, err := regexp.Compile(value)
    if err != nil {
        return fmt.Errorf("invalid regexp: %v", err)
    }
    r.Regexp = re
    return nil
}

func main() {
    var pattern RegexpValue
    
    flag.Var(&pattern, "pattern", "regular expression pattern")
    flag.Parse()
    
    if pattern.Regexp != nil {
        testStrings := []string{"hello", "world", "golang", "flag"}
        
        for _, s := range testStrings {
            if pattern.Regexp.MatchString(s) {
                fmt.Printf("'%s' matches\n", s)
            }
        }
    }
}

실행:

$ go run main.go -pattern="go.*"
'golang' matches

$ go run main.go -pattern="^[hw]"
'hello' matches
'world' matches

6. 맵 플래그

type MapValue map[string]string

func (m *MapValue) String() string {
    pairs := []string{}
    for k, v := range *m {
        pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
    }
    return strings.Join(pairs, ",")
}

func (m *MapValue) Set(value string) error {
    parts := strings.SplitN(value, "=", 2)
    if len(parts) != 2 {
        return fmt.Errorf("invalid format: expected key=value")
    }
    
    if *m == nil {
        *m = make(map[string]string)
    }
    
    (*m)[parts[0]] = parts[1]
    return nil
}

func main() {
    var labels MapValue
    
    flag.Var(&labels, "label", "label in key=value format (can be repeated)")
    flag.Parse()
    
    fmt.Println("Labels:")
    for k, v := range labels {
        fmt.Printf("  %s: %s\n", k, v)
    }
}

실행:

$ go run main.go -label=env=prod -label=region=us-east -label=version=1.0
Labels:
  env: prod
  region: us-east
  version: 1.0

플래그 검증

1. 범위 검증

func main() {
    var port int
    var timeout time.Duration
    
    flag.IntVar(&port, "port", 8080, "server port")
    flag.DurationVar(&timeout, "timeout", 30*time.Second, "request timeout")
    
    flag.Parse()
    
    // 포트 범위 검증
    if port < 1 || port > 65535 {
        fmt.Fprintf(os.Stderr, "Error: port must be between 1 and 65535\n")
        os.Exit(1)
    }
    
    // 타임아웃 검증
    if timeout < 0 {
        fmt.Fprintf(os.Stderr, "Error: timeout must be positive\n")
        os.Exit(1)
    }
    
    if timeout > 5*time.Minute {
        fmt.Fprintf(os.Stderr, "Warning: timeout is very long (%v)\n", timeout)
    }
    
    fmt.Printf("Starting server on port %d with timeout %v\n", port, timeout)
}

2. 필수 플래그

func main() {
    var name string
    var config string
    
    flag.StringVar(&name, "name", "", "application name (required)")
    flag.StringVar(&config, "config", "config.yaml", "config file")
    
    flag.Parse()
    
    // 필수 플래그 검증
    if name == "" {
        fmt.Fprintf(os.Stderr, "Error: -name is required\n")
        flag.Usage()
        os.Exit(1)
    }
    
    fmt.Printf("Name: %s, Config: %s\n", name, config)
}

3. 상호 배타적 플래그

func main() {
    var useHTTP bool
    var useHTTPS bool
    
    flag.BoolVar(&useHTTP, "http", false, "use HTTP")
    flag.BoolVar(&useHTTPS, "https", false, "use HTTPS")
    
    flag.Parse()
    
    // 둘 다 설정되면 에러
    if useHTTP && useHTTPS {
        fmt.Fprintf(os.Stderr, "Error: -http and -https are mutually exclusive\n")
        os.Exit(1)
    }
    
    // 둘 다 설정되지 않으면 기본값
    if !useHTTP && !useHTTPS {
        useHTTP = true
        fmt.Println("Using default: HTTP")
    }
    
    if useHTTP {
        fmt.Println("Using HTTP")
    } else {
        fmt.Println("Using HTTPS")
    }
}

4. 조건부 필수 플래그

func main() {
    var mode string
    var inputFile string
    var outputFile string
    
    flag.StringVar(&mode, "mode", "process", "operation mode")
    flag.StringVar(&inputFile, "input", "", "input file")
    flag.StringVar(&outputFile, "output", "", "output file")
    
    flag.Parse()
    
    // mode가 "process"일 때만 input 필수
    if mode == "process" && inputFile == "" {
        fmt.Fprintf(os.Stderr, "Error: -input is required when mode is 'process'\n")
        os.Exit(1)
    }
    
    fmt.Printf("Mode: %s, Input: %s, Output: %s\n", mode, inputFile, outputFile)
}

실전 예제

1. 백업 도구

package main

import (
    "flag"
    "fmt"
    "os"
    "path/filepath"
    "time"
)

type BackupConfig struct {
    Source      string
    Destination string
    Compress    bool
    Incremental bool
    Verbose     bool
    DryRun      bool
}

func main() {
    config := &BackupConfig{}
    
    flag.StringVar(&config.Source, "source", "", "source directory (required)")
    flag.StringVar(&config.Destination, "dest", "", "destination directory (required)")
    flag.BoolVar(&config.Compress, "compress", false, "compress backup")
    flag.BoolVar(&config.Incremental, "incremental", false, "incremental backup")
    flag.BoolVar(&config.Verbose, "verbose", false, "verbose output")
    flag.BoolVar(&config.DryRun, "dry-run", false, "dry run (no actual backup)")
    
    flag.Usage = func() {
        fmt.Fprintf(os.Stderr, "Usage: backup [OPTIONS]\n\n")
        fmt.Fprintf(os.Stderr, "A simple backup utility\n\n")
        fmt.Fprintf(os.Stderr, "OPTIONS:\n")
        flag.PrintDefaults()
        fmt.Fprintf(os.Stderr, "\nEXAMPLES:\n")
        fmt.Fprintf(os.Stderr, "  backup -source=/data -dest=/backup -compress\n")
        fmt.Fprintf(os.Stderr, "  backup -source=/data -dest=/backup -incremental -verbose\n")
    }
    
    flag.Parse()
    
    // 필수 플래그 검증
    if config.Source == "" || config.Destination == "" {
        fmt.Fprintf(os.Stderr, "Error: both -source and -dest are required\n\n")
        flag.Usage()
        os.Exit(1)
    }
    
    // 경로 존재 확인
    if _, err := os.Stat(config.Source); os.IsNotExist(err) {
        fmt.Fprintf(os.Stderr, "Error: source directory does not exist: %s\n", config.Source)
        os.Exit(1)
    }
    
    runBackup(config)
}

func runBackup(config *BackupConfig) {
    if config.Verbose {
        fmt.Println("=== Backup Configuration ===")
        fmt.Printf("Source: %s\n", config.Source)
        fmt.Printf("Destination: %s\n", config.Destination)
        fmt.Printf("Compress: %v\n", config.Compress)
        fmt.Printf("Incremental: %v\n", config.Incremental)
        fmt.Printf("Dry Run: %v\n", config.DryRun)
        fmt.Println()
    }
    
    if config.DryRun {
        fmt.Println("[DRY RUN] No actual backup will be performed")
    }
    
    timestamp := time.Now().Format("20060102-150405")
    backupName := fmt.Sprintf("backup-%s", timestamp)
    if config.Compress {
        backupName += ".tar.gz"
    }
    
    backupPath := filepath.Join(config.Destination, backupName)
    
    fmt.Printf("Creating backup: %s\n", backupPath)
    
    if !config.DryRun {
        // 실제 백업 로직
        fmt.Println("Backup completed successfully")
    }
}

2. 로그 분석 도구

package main

import (
    "bufio"
    "flag"
    "fmt"
    "os"
    "regexp"
    "strings"
)

func main() {
    var logFile string
    var pattern string
    var ignoreCase bool
    var lineNumbers bool
    var count bool
    var invert bool
    
    flag.StringVar(&logFile, "file", "", "log file to analyze (required)")
    flag.StringVar(&pattern, "pattern", "", "search pattern (required)")
    flag.BoolVar(&ignoreCase, "i", false, "case-insensitive search")
    flag.BoolVar(&lineNumbers, "n", false, "show line numbers")
    flag.BoolVar(&count, "c", false, "count matches only")
    flag.BoolVar(&invert, "v", false, "invert match (show non-matching lines)")
    
    flag.Parse()
    
    if logFile == "" || pattern == "" {
        fmt.Fprintf(os.Stderr, "Error: both -file and -pattern are required\n")
        flag.Usage()
        os.Exit(1)
    }
    
    // 정규표현식 준비
    if ignoreCase {
        pattern = "(?i)" + pattern
    }
    
    re, err := regexp.Compile(pattern)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: invalid pattern: %v\n", err)
        os.Exit(1)
    }
    
    // 파일 열기
    file, err := os.Open(logFile)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: cannot open file: %v\n", err)
        os.Exit(1)
    }
    defer file.Close()
    
    // 분석
    scanner := bufio.NewScanner(file)
    lineNum := 0
    matches := 0
    
    for scanner.Scan() {
        lineNum++
        line := scanner.Text()
        
        matched := re.MatchString(line)
        if invert {
            matched = !matched
        }
        
        if matched {
            matches++
            if !count {
                if lineNumbers {
                    fmt.Printf("%d: %s\n", lineNum, line)
                } else {
                    fmt.Println(line)
                }
            }
        }
    }
    
    if count {
        fmt.Printf("Total matches: %d\n", matches)
    }
}

실행:

$ go run log-analyzer.go -file=app.log -pattern="ERROR" -n
15: ERROR: Connection failed
42: ERROR: Invalid request
78: ERROR: Database timeout

$ go run log-analyzer.go -file=app.log -pattern="ERROR" -c
Total matches: 3

$ go run log-analyzer.go -file=app.log -pattern="ERROR" -v -c
Total matches: 97

3. HTTP 클라이언트 도구

package main

import (
    "flag"
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
    "time"
)

type HeaderFlags []string

func (h *HeaderFlags) String() string {
    return strings.Join(*h, ", ")
}

func (h *HeaderFlags) Set(value string) error {
    *h = append(*h, value)
    return nil
}

func main() {
    var url string
    var method string
    var headers HeaderFlags
    var body string
    var timeout time.Duration
    var verbose bool
    
    flag.StringVar(&url, "url", "", "URL to request (required)")
    flag.StringVar(&method, "method", "GET", "HTTP method")
    flag.Var(&headers, "header", "HTTP header (can be repeated)")
    flag.StringVar(&body, "body", "", "request body")
    flag.DurationVar(&timeout, "timeout", 30*time.Second, "request timeout")
    flag.BoolVar(&verbose, "verbose", false, "verbose output")
    
    flag.Parse()
    
    if url == "" {
        fmt.Fprintf(os.Stderr, "Error: -url is required\n")
        flag.Usage()
        os.Exit(1)
    }
    
    // HTTP 클라이언트 생성
    client := &http.Client{
        Timeout: timeout,
    }
    
    // 요청 생성
    var bodyReader io.Reader
    if body != "" {
        bodyReader = strings.NewReader(body)
    }
    
    req, err := http.NewRequest(method, url, bodyReader)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err)
        os.Exit(1)
    }
    
    // 헤더 설정
    for _, header := range headers {
        parts := strings.SplitN(header, ":", 2)
        if len(parts) == 2 {
            req.Header.Set(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
        }
    }
    
    if verbose {
        fmt.Printf("=== Request ===\n")
        fmt.Printf("%s %s\n", method, url)
        for k, v := range req.Header {
            fmt.Printf("%s: %s\n", k, strings.Join(v, ", "))
        }
        if body != "" {
            fmt.Printf("\n%s\n", body)
        }
        fmt.Println()
    }
    
    // 요청 실행
    resp, err := client.Do(req)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error making request: %v\n", err)
        os.Exit(1)
    }
    defer resp.Body.Close()
    
    // 응답 출력
    fmt.Printf("=== Response ===\n")
    fmt.Printf("Status: %s\n", resp.Status)
    
    if verbose {
        fmt.Println("Headers:")
        for k, v := range resp.Header {
            fmt.Printf("  %s: %s\n", k, strings.Join(v, ", "))
        }
        fmt.Println()
    }
    
    fmt.Println("Body:")
    io.Copy(os.Stdout, resp.Body)
    fmt.Println()
}

실행:

$ go run http-client.go -url=https://api.github.com/users/golang \
    -header="Accept: application/json" \
    -verbose

=== Request ===
GET https://api.github.com/users/golang
Accept: application/json

=== Response ===
Status: 200 OK
Headers:
  Content-Type: application/json
  ...

Body:
{"login":"golang","id":4314092,...}

일반적인 실수

1. Parse() 호출 누락

// ❌ 나쁜 예
func main() {
    name := flag.String("name", "Guest", "name")
    fmt.Println(*name) // 항상 "Guest" 출력
    // flag.Parse() 누락!
}

// ✅ 좋은 예
func main() {
    name := flag.String("name", "Guest", "name")
    flag.Parse() // 반드시 호출
    fmt.Println(*name)
}

2. Parse() 전 플래그 값 접근

// ❌ 나쁜 예
func main() {
    name := flag.String("name", "Guest", "name")
    if *name == "" { // Parse 전 접근
        fmt.Println("Name is empty")
    }
    flag.Parse()
}

// ✅ 좋은 예
func main() {
    name := flag.String("name", "Guest", "name")
    flag.Parse()
    if *name == "" {
        fmt.Println("Name is empty")
    }
}

3. 포인터 역참조 잊어버림

// ❌ 나쁜 예
func main() {
    name := flag.String("name", "Guest", "name")
    flag.Parse()
    fmt.Println(name) // 포인터 주소 출력
}

// ✅ 좋은 예
func main() {
    name := flag.String("name", "Guest", "name")
    flag.Parse()
    fmt.Println(*name) // 역참조
}

// ✅ 또는 Var 사용
func main() {
    var name string
    flag.StringVar(&name, "name", "Guest", "name")
    flag.Parse()
    fmt.Println(name) // 직접 사용
}

4. 불린 플래그 값 지정 오류

// 실행 시:
$ program -verbose true  # "true" 다음 인자로 간주됨

// ✅ 올바른 사용
$ program -verbose         # true
$ program -verbose=true    # true
$ program -verbose=false   # false

5. 플래그 검증 누락

// ❌ 나쁜 예
func main() {
    port := flag.Int("port", 8080, "port")
    flag.Parse()
    // 범위 검증 없음
}

// ✅ 좋은 예
func main() {
    port := flag.Int("port", 8080, "port")
    flag.Parse()
    
    if *port < 1 || *port > 65535 {
        fmt.Fprintf(os.Stderr, "Error: invalid port: %d\n", *port)
        os.Exit(1)
    }
}

6. 에러 메시지 부족

// ❌ 나쁜 예
if name == "" {
    os.Exit(1)
}

// ✅ 좋은 예
if name == "" {
    fmt.Fprintf(os.Stderr, "Error: -name is required\n")
    flag.Usage()
    os.Exit(1)
}

7. 글로벌 FlagSet 혼용

// ❌ 나쁜 예
func main() {
    fs := flag.NewFlagSet("myapp", flag.ExitOnError)
    fs.String("name", "", "name")
    
    flag.String("age", "", "age") // 글로벌 flag와 혼용!
    
    fs.Parse(os.Args[1:])
}

// ✅ 좋은 예
func main() {
    fs := flag.NewFlagSet("myapp", flag.ExitOnError)
    fs.String("name", "", "name")
    fs.String("age", "", "age") // 같은 FlagSet 사용
    
    fs.Parse(os.Args[1:])
}

베스트 프랙티스

1. Var 버전 사용

// ✅ Var 버전 권장 (포인터 역참조 불필요)
var name string
var port int
var verbose bool

flag.StringVar(&name, "name", "Guest", "name")
flag.IntVar(&port, "port", 8080, "port")
flag.BoolVar(&verbose, "verbose", false, "verbose")

flag.Parse()

// 변수 직접 사용
fmt.Printf("Name: %s, Port: %d, Verbose: %v\n", name, port, verbose)

2. 설정 구조체 사용

type Config struct {
    Host    string
    Port    int
    Timeout time.Duration
    Verbose bool
}

func parseFlags() *Config {
    cfg := &Config{}
    
    flag.StringVar(&cfg.Host, "host", "localhost", "server host")
    flag.IntVar(&cfg.Port, "port", 8080, "server port")
    flag.DurationVar(&cfg.Timeout, "timeout", 30*time.Second, "timeout")
    flag.BoolVar(&cfg.Verbose, "verbose", false, "verbose output")
    
    flag.Parse()
    
    return cfg
}

func main() {
    config := parseFlags()
    fmt.Printf("%+v\n", config)
}

3. 명확한 플래그 이름

// ✅ 좋은 예
flag.StringVar(&outputFile, "output-file", "", "output file path")
flag.IntVar(&maxRetries, "max-retries", 3, "maximum retry attempts")
flag.BoolVar(&enableCompression, "enable-compression", false, "enable compression")

// ❌ 나쁜 예
flag.StringVar(&outputFile, "o", "", "")
flag.IntVar(&maxRetries, "r", 3, "")
flag.BoolVar(&enableCompression, "c", false, "")

4. 짧은 형식과 긴 형식 모두 제공

func main() {
    var verbose bool
    
    flag.BoolVar(&verbose, "verbose", false, "verbose output")
    flag.BoolVar(&verbose, "v", false, "verbose output (shorthand)")
    
    flag.Parse()
}

// 사용:
// program -verbose
// program -v

5. 도움말 메시지 작성

flag.Usage = func() {
    fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] <command>\n\n", os.Args[0])
    fmt.Fprintf(os.Stderr, "Description:\n")
    fmt.Fprintf(os.Stderr, "  A tool for processing data files\n\n")
    fmt.Fprintf(os.Stderr, "OPTIONS:\n")
    flag.PrintDefaults()
    fmt.Fprintf(os.Stderr, "\nCOMMANDS:\n")
    fmt.Fprintf(os.Stderr, "  process   Process input files\n")
    fmt.Fprintf(os.Stderr, "  validate  Validate file format\n")
    fmt.Fprintf(os.Stderr, "\nEXAMPLES:\n")
    fmt.Fprintf(os.Stderr, "  %s -input=data.csv process\n", os.Args[0])
    fmt.Fprintf(os.Stderr, "  %s -verbose validate input.json\n", os.Args[0])
}

6. 환경 변수 폴백

func getEnvOrFlag(envVar, flagValue, defaultValue string) string {
    if flagValue != "" && flagValue != defaultValue {
        return flagValue // 플래그 우선
    }
    if envValue := os.Getenv(envVar); envValue != "" {
        return envValue // 환경 변수
    }
    return defaultValue // 기본값
}

func main() {
    var apiKey string
    flag.StringVar(&apiKey, "api-key", "", "API key")
    flag.Parse()
    
    apiKey = getEnvOrFlag("API_KEY", apiKey, "")
    
    if apiKey == "" {
        fmt.Fprintf(os.Stderr, "Error: API key required (use -api-key or API_KEY env var)\n")
        os.Exit(1)
    }
}

7. 플래그 그룹화

type ServerConfig struct {
    Host string
    Port int
    TLS  bool
}

type DatabaseConfig struct {
    Host     string
    Port     int
    Name     string
    User     string
    Password string
}

type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
    Verbose  bool
}

func parseFlags() *Config {
    cfg := &Config{}
    
    // Server flags
    flag.StringVar(&cfg.Server.Host, "server-host", "0.0.0.0", "server host")
    flag.IntVar(&cfg.Server.Port, "server-port", 8080, "server port")
    flag.BoolVar(&cfg.Server.TLS, "server-tls", false, "enable TLS")
    
    // Database flags
    flag.StringVar(&cfg.Database.Host, "db-host", "localhost", "database host")
    flag.IntVar(&cfg.Database.Port, "db-port", 5432, "database port")
    flag.StringVar(&cfg.Database.Name, "db-name", "", "database name")
    flag.StringVar(&cfg.Database.User, "db-user", "", "database user")
    flag.StringVar(&cfg.Database.Password, "db-password", "", "database password")
    
    // Global flags
    flag.BoolVar(&cfg.Verbose, "verbose", false, "verbose output")
    
    flag.Parse()
    
    return cfg
}

정리

  • 기본 사용: String, Int, Bool, Duration 등 타입별 플래그
  • FlagSet: 서브커맨드 구현, 에러 처리 모드
  • 커스텀 타입: Value 인터페이스 구현으로 확장
  • 검증: 범위, 필수, 상호 배타적, 조건부 검증
  • 실전: 백업 도구, 로그 분석, HTTP 클라이언트
  • 실수: Parse 누락, 포인터 역참조, 검증 누락, 에러 메시지 부족
  • 베스트: Var 버전, 구조체, 명확한 이름, 도움말, 환경 변수 통합
  • 원칙: 타입 안전, 명확한 에러, 사용자 친화적