os.Args
os.Args は文字列のスライスです。os.Args[0] はプログラムの名前で、残りはシェルが渡したとおりの引数です。
エディタのArgsパネルでは、各フィールドが1つの引数になり、そのまま渡されます。3つのフィールドに hello、two words、-v を入れてみてください。プログラムは3つの引数を受け取り、two words は空白を含んだまま1つの引数です。ターミナルではシェルが分割するので、同じことは go run . hello "two words" -v になります。
インデックスを使う前に必ず len(os.Args) をチェックします。引数がないときの os.Args[1] は index out of range [1] with length 1 でpanicします。
引数は文字列です。ユーザーは何でも入力するので、数値は strconv.Atoi や strconv.ParseFloat で変換し、エラーを処理します。
flagパッケージ
-port 8080 -verbose のようなオプションには flag を使います。パースし、型を変換し、エラーを報告し、ヘルプメッセージを生成してくれます。
引数なしでは hello, world を1回表示します。Argsパネルで、2つのフィールドに -name と Gopher を、さらに3つのフィールドに -count=3、-loud、extra を入れてみてください。プログラムは HELLO, GOPHER! を3回表示し、remaining args: [extra] と表示します。
フラグの仕組みは次のとおりです。
- 各定義関数(
flag.String、flag.Int、flag.Bool、flag.Float64、flag.Duration、flag.Uint64など)は、名前、デフォルト値、使い方の文字列を受け取り、ポインタ を返します。値はflag.Parse()の後に*nameで読みます。 Var版は既存の変数に結びつけます:flag.IntVar(&cfg.Port, "port", 8080, "port")。設定用の構造体にはこちらのほうがすっきりします。- ユーザーは
-name value、-name=value、--name value、--name=valueのどれでも書けます。Goはダッシュ1つと2つを区別しません。 - 真偽値に値を渡すには
=が必要。-loudはtrueに、-loud=falseはfalseにしますが、-loud falseはtrueにしてfalseを位置引数として残します。 - パースは最初のフラグでない引数(または
--)で止まる。prog file.txt -vは-vを位置引数として扱います。フラグは先に置きます。 flag.Args()は残った位置引数を返し、flag.NArg()はその数を、flag.Arg(i)はそのうちの1つを返します。
未知のフラグや不正な値はエラーと使い方を表示し、ステータス2で終了します。-h や -help は使い方を表示してステータス0で終了します(Go 1.15以降)。使い方のテキストは定義から生成されます。
Usage of greet:
-count int
how many times (default 1)
-delay duration
pause between greetings, e.g. 10ms
-loud
shout the greeting
-name string
who to greet (default "world")
flag.PrintDefaults() を呼ぶ前に独自のヘッダーを表示するには、flag.Usage に関数を設定します。
FlagSetによるサブコマンド
git commit -m msg のようなツールには、独自のフラグを持つサブコマンドがあります。サブコマンドごとに flag.FlagSet を作り、最初の引数に対するswitchで1つを選びます。
flag.ContinueOnError は、Parse が終了する代わりにエラーを返すようにするので、関数がテストしやすくなります。run は os.Args を読むのではなく引数としてそれを受け取るので、テストから直接 run([]string{"list", "-all"}) を呼べます。Argsパネルで list と -all を試すか、エラーの経路を見るには delete を試してください。
ネストしたコマンド、シェルの補完、ドキュメントの生成を持つ大きなCLIでは、多くのプロジェクトがサードパーティの github.com/spf13/cobra を使います。小さなツールなら標準の flag パッケージで十分です。
環境変数
os.Getenvは、変数がないときも空文字列に設定されているときも""を返します。os.LookupEnvはそれらを区別します。- 値は常に文字列です。起動時に変換して検証し、リクエストの途中ではなく、わかりやすいメッセージとともに失敗させます。
os.Setenvは現在のプロセスと、その後に開始する子プロセスに影響します。自分を起動したシェルの環境は変更できません。os.Environ()はすべての変数を"KEY=value"の文字列として返します。
設定のよくある構成は、人が入力するものにはフラグ、デプロイの設定(コンテナのプラットフォームが設定します)には環境変数を使い、フラグが環境変数を、環境変数がデフォルトを上書きするというものです。
終了コードとos.Exit
Goのプログラムは、main が戻るとステータス0で終了します。os.Exit(code) はそのステータスでただちにプロセスを終了します。慣習として、0は成功、1は一般的なエラー、2は使い方のエラーです(flag パッケージは2を使います)。
os.Exit はdeferした関数を実行しません。 ファイルはフラッシュされず、defer の後始末も飛ばされます。log.Fatal は os.Exit(1) を呼ぶので同じ効果があります。os.Exit は main の最後の1か所にとどめ、サブコマンドの例のように、実際のプログラムはエラーを返す run 関数に置きます。
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
エラーメッセージは os.Stdout ではなく os.Stderr に書きます。そうすれば、出力をファイルにリダイレクトしたり別のコマンドにパイプしたりしても見えたままです。回復されなかったpanicはステータス2で終了します。
よくある間違い
- 長さをチェックせずに
os.Argsにインデックスでアクセスする。 引数がないとpanicします。 flag.Parse()の前にフラグを読む。 デフォルト値しか得られません。*を忘れる。fmt.Println(port)は値ではなく0xc000012345のようなアドレスを表示します。- 位置引数の後にフラグを置く。
prog input.txt -vは-vをパースしません。 - 真偽値のフラグに
-verbose falseと書く。-verbose=falseと書きます。 - プログラムの深いところで
os.Exitやlog.Fatalを呼ぶ。 deferした後始末が実行されず、コードをテストできません。エラーをmainまで返します。
よくある質問
Goでコマンドライン引数を取得するには?
os.Args は、インデックス0にプログラム名、その後に引数を持つ []string です。os.Args[1:] がユーザーの入力した引数です。インデックスを使う前に len(os.Args) をチェックしないと、引数がないときにプログラムがpanicします。
Goでflagパッケージを使うには?
フラグを宣言し、flag.Parse() を呼んでから読みます:port := flag.Int("port", 8080, "port to listen on")、flag.Parse()、fmt.Println(*port)。関数はポインタを返します。ユーザーは -port=9000、-port 9000、--port 9000 のように書き、-h で自動生成された使い方が表示されます。
Goで環境変数を読むには?
os.Getenv("HOME") は値を返し、変数が設定されていなければ空文字列を返します。未設定と空の値に設定されている場合を区別するには v, ok := os.LookupEnv("HOME") を使います。os.Setenv は現在のプロセスと、その後に開始する子プロセスの環境を変更します。
Goのos.Exitはdeferした関数を実行しますか?
実行しません。os.Exit は指定したステータスコードでただちにプロセスを終了し、deferした呼び出しは実行されないので、バッファされた出力が失われたり、ファイルがフラッシュされなかったりすることがあります。よくあるパターンは func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) } } で、実際の処理とdeferはすべて run の中に置きます。