こんにちは、かつコーチです。
今回は実用寄りに、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.Stringやflag.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 commitやgit pushのように、コマンドごとにオプションを切り替えたいツールで役立つ書き方です。
サブコマンドが増えてきたら、このflag.NewFlagSetの考え方を土台に処理を整理していくとよいでしょう。
まとめ
この記事のポイント
flagパッケージは標準ライブラリだけでコマンドライン引数を扱える仕組みflag.Stringやflag.Intでフラグを定義し、必ずflag.Parse()を呼ぶ- 返り値はポインタ(値の住所)なので、使うときは
*で中身を参照する flag.StringVarを使えば*なしで扱える変数として受け取れる- 複数のサブコマンドを扱うなら
flag.NewFlagSetが便利
次に読むべき記事
flagパッケージの基本を押さえたら、次はこれまでの内容を組み合わせて、実践編「実践:ファイル一括処理CLIツールを作る」に挑戦してみましょう。