flag: add All iterators and Flag.IsSet
要約
概要
flagパッケージに、コマンドラインフラグを列挙するための新しいイテレータAll() iter.Seq[*Flag]と、フラグが実際にセットされたかどうかを示すFlag.IsSetフィールドを追加する提案です。
ステータス変更
active → likely_accept
2026年7月の週次proposal reviewにおいて、aclementsおよびbradfitzらの議論を経て「likely accept」と判断されました。当初検討されていたAll() iter.Seq2[*Flag, bool](値とセット済みかどうかのペアを返す)案は、iter.Seqより複雑になることや、将来的な拡張性の観点から見送られ、シンプルなiter.Seq[*Flag]を返すAll()と、Flag構造体にIsSet boolフィールドを追加する方式に落ち着きました。この形であればFlag.Set(value string)という命令的なメソッドを将来追加する余地も残せる、という点が決め手になりました。
技術的背景
現状の問題点
flagパッケージには従来からVisit(セットされたフラグのみを巡回)とVisitAll(全フラグを巡回)というコールバック形式のイテレータがありますが、これらはfunc(*Flag)というシグネチャで、Go 1.23で導入されたrange-over-func(iter.Seq)と互換性がありません。また、あるフラグが「セットされているか」を判定するには、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
}
提案された解決策
flagパッケージに以下を追加します。
type Flag struct {
// ...(既存フィールド)
// IsSet indicates that this flag was set.
IsSet bool
}
// All yields all command-line flags, in lexicographical order.
func All() iter.Seq[*Flag]
// All yields the flags in lexicographical order.
func (f *FlagSet) All() iter.Seq[*Flag]
パッケージレベルのAll()とトップレベルのFlagSet用のAll()の両方を提供し、既存のVisit/VisitAllとの一貫性を保ちつつ、for range構文で直接利用できるようにします。
これによって何ができるようになるか
for f := range flag.All()という自然なfor-range構文でフラグを走査でき、各フラグのIsSetフィールドを見るだけでセット済みかどうかを判定できます。これにより、両方のマップを別々に走査して突き合わせる必要がなくなり、コードが簡潔かつ効率的になります。
- サブコマンド実装で「グローバルフラグの前に置かれた不正なフラグ」を検出する処理の簡略化
- 環境変数からのフォールバック設定など、未セットのフラグのみを対象にした処理
- デバッグ・ロギング目的で全フラグとその値・セット状態を一覧表示する処理
コード例
// Before: Visitとルックアップの組み合わせが必要
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-rangeに
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つの独立したイテレータ、あるいはAll() iter.Seq2[*Flag, bool](値とセット状態のペア)が検討されたが、mvdanから「formalとactualという2つの内部マップを都度突き合わせる実装コストが生じる」との指摘があった。 aclementsとbradfitzによるproposal review内で、シンプルさを優先し、Flag構造体にIsSet boolフィールドを追加する案が採用された。フィールド名を命令形のSetではなくIsSetにすることで、将来(*Flag).Set(value string)のような設定用メソッドを追加する余地を残した。- 提案者の
earthboundkidは「同じFlagが複数のFlagSetに属し得る」ケースを懸念しIsSet(string) boolという別APIを提案したが、aclementsが「FlagをFlagSetに後から追加する手段がないため、単一のFlagSetにしか属し得ない」と指摘し、この懸念は解消された。 - 類似の重複proposal(#80026、
adonovan提案)が別途proposal evaluation boardに乗っていたことが判明し、プロセスの一貫性・透明性の欠如についてadonovan自身が言及する一幕もあった。最終的に本issue(#65675)の方でAPI設計がまとまり、likely acceptとなった。