【Go】flagパッケージでコマンドライン引数を扱う基本

Go

こんにちは、かつコーチです。

今回は実用寄りに、CLIツール作りに欠かせないflagパッケージの基本を解説します。

go run main.go -name=Taroのような引数を受け取りたいとき、最初に使う標準ライブラリです。

flagパッケージとは?

flagパッケージの定義

flagパッケージとは、Go標準ライブラリに含まれる、コマンドライン引数(実行時に-name=Taroのように追加で渡す値)を扱うための道具です。

外部ライブラリなしで、オプション付き引数を簡単に扱えます。

なぜflagパッケージが必要なのか

CLIツールを作るとき、実行のたびにコードを書き換えるのは非効率です。

-env=productionのように実行時に値を切り替えられれば、同じプログラムを様々な条件で動かせます。

たとえば本番用と開発用で接続先を変えたい場合も、コードを直接編集せずに引数だけで切り替えられるようになります。

基本の書き方

手順1: flag.Stringでフラグを定義する

package main

import (
	"flag"
	"fmt"
)

func main() {
	// 第1引数: フラグ名, 第2引数: デフォルト値, 第3引数: 説明文
	name := flag.String("name", "ゲスト", "挨拶する相手の名前")

	flag.Parse() // 必ずこれを呼ぶ

	fmt.Printf("こんにちは、%sさん\n", *name)
}

go run main.goと実行すると「こんにちは、ゲストさん」と表示されます。

go run main.go -name=かつコーチとすれば表示が変わります。

手順2: flag.Intやflag.Boolも同じ要領で使う

package main

import (
	"flag"
	"fmt"
)

func main() {
	count := flag.Int("count", 1, "繰り返す回数")
	verbose := flag.Bool("verbose", false, "詳細ログを表示するか")

	flag.Parse()

	for i := 0; i < *count; i++ {
		fmt.Println("処理を実行中...")
	}
	if *verbose {
		fmt.Println("詳細ログ: 処理が完了しました")
	}
}

-count=3 -verboseのように実行すると、3回のメッセージの後に詳細ログも表示されます。

つまずきやすい設定・注意点

flag.Stringflag.Intは、値そのものではなくポインタ(値そのものではなく「値がしまってある住所」を渡す仕組み)を返します。

そのため値を使うときは*nameのように、アスタリスクで中身を取り出す必要があります。

また、すべてのフラグを定義し終えたら、必ず1回だけflag.Parse()を呼び出してください。

よくあるつまずきポイント・エラー対処

ポインタであることを忘れて型エラーになる話

私がflagパッケージを初めて使ったとき、真っ先にハマったのがポインタの扱いでした。

❌ Before

name := flag.String("name", "ゲスト", "挨拶する相手の名前")
flag.Parse()
fmt.Printf("こんにちは、%sさん\n", name) // *をつけ忘れた

実行すると、期待した表示にならず、次のような出力になりました。

こんにちは、0xc0000123456さん

nameの中身は文字列ではなく、文字列が格納されているメモリの住所そのものだったのです。

「なんで文字列じゃなくてアドレスみたいな値が出るんだ」と混乱し、ドキュメントを読み返してようやく、返り値が*string型だと気づきました。

✅ After

name := flag.String("name", "ゲスト", "挨拶する相手の名前")
flag.Parse()
fmt.Printf("こんにちは、%sさん\n", *name) // *で中身を取り出す

なおflag.StringVarを使えば、あらかじめ用意した変数に直接書き込んでもらう形も選べます。

var name string
flag.StringVar(&name, "name", "ゲスト", "挨拶する相手の名前")
flag.Parse()
fmt.Printf("こんにちは、%sさん\n", name) // こちらは*不要

こちらは変数の住所を渡す形なので、使うときに*は不要です。

flag.Parse()を呼び忘れるミス

もう1つ、私が実際にやってしまったのがflag.Parse()の呼び忘れです。

❌ Before

name := flag.String("name", "ゲスト", "挨拶する相手の名前")
// flag.Parse()を呼び忘れている
fmt.Println(*name)

✅ After

name := flag.String("name", "ゲスト", "挨拶する相手の名前")
flag.Parse()
fmt.Println(*name)

flag.Parse()を呼び忘れると、-name=かつコーチと指定しても反映されず、常にデフォルト値のままになります。

私も一度、この呼び忘れで「フラグが全く効かない」と数分悩んだことがあります。

すべてのフラグ定義の直後にflag.Parse()を書く、という順番を体に覚えさせておくのがおすすめです。

応用・一歩先の使い方

サブコマンドを持つCLIツールを作る場合は、flag.NewFlagSetでコマンドごとに異なるフラグ集合を定義できます。

countCmd := flag.NewFlagSet("count", flag.ExitOnError)
num := countCmd.Int("num", 1, "繰り返す回数")
countCmd.Parse(os.Args[2:])

git commitgit pushのように、コマンドごとにオプションを切り替えたいツールで役立つ書き方です。

サブコマンドが増えてきたら、このflag.NewFlagSetの考え方を土台に処理を整理していくとよいでしょう。

まとめ

この記事のポイント

  • flagパッケージは標準ライブラリだけでコマンドライン引数を扱える仕組み
  • flag.Stringflag.Intでフラグを定義し、必ずflag.Parse()を呼ぶ
  • 返り値はポインタ(値の住所)なので、使うときは*で中身を参照する
  • flag.StringVarを使えば*なしで扱える変数として受け取れる
  • 複数のサブコマンドを扱うならflag.NewFlagSetが便利

次に読むべき記事

flagパッケージの基本を押さえたら、次はこれまでの内容を組み合わせて、実践編「実践:ファイル一括処理CLIツールを作る」に挑戦してみましょう。

タイトルとURLをコピーしました