[Go] command-line flag
개요
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 버전, 구조체, 명확한 이름, 도움말, 환경 변수 통합
- 원칙: 타입 안전, 명확한 에러, 사용자 친화적