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

Go Proposal Weekly Digest

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

#65675accepted

flag: add All iterators and Flag.IsSet

ステータス変更: likely_accept accepted

要約

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

概要

flagパッケージに、Goのrange-over-func(iter.Seq)に対応した新しいイテレータAll()と、フラグが実際にセットされたかどうかを示すFlag.IsSetフィールドを追加する提案です。

ステータス変更

likely_acceptaccepted
2026年7月15日の提案レビューで「likely accept」となった後、2026年7月22日に実装CL(go.dev/cl/804020)が提出され、大きな異論もなかったことから、7月23日のレビューで「no change in consensus」として正式にacceptedとなりました。

技術的背景

現状の問題点

flagパッケージには既にVisit(セットされたフラグのみ)とVisitAll(全フラグ)というコールバック形式のイテレータメソッドがありますが、これらはfunc(*Flag)を受け取るだけでboolを返さないため、Go 1.23で導入されたrange-over-func(iter.Seq)と互換性がありません。そのため、以下のような煩雑なクロージャとエラー変数の組み合わせが必要でした。

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
}

また、あるフラグが「セットされているか」を判定する公式なAPIが存在せず、内部的にはformal(定義済み)とactual(セット済み)という2つのmapで管理されているため、外部から判定するにはVisitVisitAllの結果を突き合わせる必要がありました。

提案された解決策

議論は当初、単一のAll() iter.Seq2[*Flag, bool](boolでセット状態を表す)案と、All()/AllSet()の2イテレータ案の間で揺れましたが、最終的にはよりシンプルな設計に落ち着きました。Flag構造体にIsSet boolフィールドを追加し、All()*Flagのみを返すiter.Seq[*Flag]とすることで、イテレータ自体をシンプルに保ちつつ状態情報も取得可能にしています。

type Flag struct {
    // ...既存フィールド...
    IsSet bool // このフラグがセットされたかどうか
}
func All() iter.Seq[*Flag]
func (*FlagSet) All() iter.Seq[*Flag]

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

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)
    }
})
// After: range-over-funcと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)
    }
}

議論のハイライト

  • 当初(2024年4月)はAll()AllSet()という2つの独立したイテレータが提案されたが、命名(特に"AllSet")に不満が残った。
  • @earthboundkidがiter.Seq2[*Flag, bool]案を提示したが、@mvdanが「formalactualの2つのmapを両方参照する必要があり実装コストが増す」と指摘し議論が停滞。
  • 2026年6月、同種の提案(#80026、@adonovan提出)がレビューボードに載ったことをきっかけに本issueの議論が再燃。プロセスの一貫性・透明性の欠如についても言及された。
  • @aclementsが「FlagIsSet boolフィールドを追加しAll()はシンプルなiter.Seq[*Flag]にすべき」と提案し、これが採用の軸となった。
  • @bradfitzが命名をSetではなくIsSetとすることを提案。将来的に(*Flag).Set(value string)という命令的メソッドを追加する余地を残すための配慮。
  • Flagが複数のFlagSetに属する可能性についての懸念が挙がったが、Flagは生成元のFlagSetにのみ紐づくため問題ないと確認された。
  • 実装CL提出後、Flag構造体へのフィールド追加によりgo/analysis/passes/compositeのテストが失敗し、別CL(go.dev/cl/804080)で修正された。

関連リンク