メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

#65675active

flag: add All, AllSet iterators

新規提案

要約

AIによる要約であり、誤りを含む場合があります。

概要

flagパッケージにコールバック方式のVisit/VisitAllに代わる、Go 1.23で導入されたrange-over-func(イテレータ)に対応した新しいイテレータAPIを追加する提案です。あわせて、フラグが「設定済みかどうか」を扱いやすくするためのAPI変更も議論されています。

ステータス変更

(新規)active
2026年7月8日の週次Proposal Reviewミーティングで、本Issue(#65675)が正式にactiveカラムへ追加され、今後のレビュー対象となりました。なお、ほぼ同内容の重複提案であった#80026(FlagSet.All() iter.Seq2[*Flag, bool])は同じミーティングで「duplicate」として declined となり、議論はこの#65675に一本化されました。

技術的背景

現状の問題点

flagパッケージのVisitVisitAllはコールバック関数を渡す古いスタイルのイテレーション方式で、boolを返さないため、Go 1.23で追加されたrange-over-funcfor rangeに関数を渡せる仕組み)と互換性がありません。また、あるフラグが「明示的に設定されたか」を調べるには、内部的にformal(全フラグ)とactual(設定済みフラグ)という2つのマップを突き合わせる必要があり、利用者側では両方を走査してsetを判定する煩雑なコード(例: flagx.listVisitedFlagNames)を書かざるを得ませんでした。

提案された解決策

当初はAll() iter.Seq[*Flag]AllSet() iter.Seq[*Flag]という2種類のイテレータを追加する案でしたが、議論の結果、レビューチーム(@aclements, @bradfitz)から以下のシンプルな設計が提示されました。

type Flag struct {
    Name     string
    Usage    string
    Value    Value
    DefValue string
    IsSet    bool // 追加: このフラグが設定済みかどうか
}
func All() iter.Seq[*Flag]
func (*FlagSet) All() iter.Seq[*Flag]

Flag構造体にIsSet boolフィールドを追加することで、iter.Seq2[*Flag, bool]のような複雑な戻り値にせず、単一のiter.Seq[*Flag]だけで「全フラグ」と「設定状態」の両方を扱えるようにする案です。命令形のSetという名前は将来(*Flag).Set(value string)のような設定用メソッドのために温存し、フィールド名はIsSetとすることで区別しています。

これによって何ができるようになるか

for range構文で直接フラグ集合をループでき、Visit/VisitAllのコールバック地獄や、設定済みフラグを判定するための二重ループが不要になります。特に、サブコマンド実装で「あるフラグがどのFlagSetに属し、かつ設定済みか」を判定するような用途(例: CLIツールでグローバルフラグとサブコマンドフラグを区別するエラーチェック)で有用です。

コード例

// Before: Visit + VisitAll を組み合わせて判定する煩雑な書き方
var err error
cmdFlags.Visit(func(f *flag.Flag) {
    if globalFlags.Lookup(f.Name) == nil {
        err = fmt.Errorf("flag -%s belongs to subcommand but is placed before it", f.Name)
    }
})
if err != nil {
    return err
}
// After: All() と IsSet フィールドを使った書き方(レビューで議論中の案)
for f := range cmdFlags.All() {
    if f.IsSet && globalFlags.Lookup(f.Name) == nil {
        return fmt.Errorf("flag -%s belongs to subcommand but is placed before it", f.Name)
    }
}

議論のハイライト

  • 当初案(All/AllSetの2イテレータ)に対し、@mvdan(コアメンバー)は「formalactualという2つの内部マップを両方参照する必要がある」と指摘し、マップ統合をしない限り2つの独立イテレータの方が実装が簡潔であると述べた。
  • 重複提案#80026ではiter.Seq2[*Flag, bool]案(All(*Flag, bool)のペアを返す)が提示されたが、@aclementsは「Flag.Set boolフィールドを追加してシンプルなiter.Seq[*Flag]にする方が良いのでは」とコメントし、こちらの方向にレビューが収束した。
  • @bradfitzが命名について、フィールド名をSetではなくIsSetとすることで、将来(*Flag).Set(value string)という命令形メソッドを追加する余地を残す案を提示し、コアチームで合意された。
  • Issue作成者の@earthboundkidは「あるFlagが複数のFlagSetに属する可能性がある」ことを懸念し、IsSetをフィールドではなくFlagSet.IsSet(name string) boolという関数にすべきと提案したが、@aclementsは「Flagを複数のFlagSetに追加する手段が現状APIには存在しないため、単一のFlagSetにのみ属する」と回答し、フィールド案が維持される見込み。
  • 重複していた#80026は「duplicate」として declined され、議論は本Issue(#65675)に一本化された。

関連リンク