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

Go Proposal Weekly Digest

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

#65675likely_accept

flag: add All iterators and Flag.IsSet

ステータス変更: active likely_accept

要約

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

概要

flagパッケージに、コマンドラインフラグを列挙するための新しいイテレータAll() iter.Seq[*Flag]と、フラグが実際にセットされたかどうかを示すFlag.IsSetフィールドを追加する提案です。

ステータス変更

activelikely_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)と互換性がありません。また、あるフラグが「セットされているか」を判定するには、VisitVisitAllを両方回して集合を突き合わせる必要があり、非効率かつ煩雑なコードになっていました。

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から「formalactualという2つの内部マップを都度突き合わせる実装コストが生じる」との指摘があった。
  • aclementsbradfitzによるproposal review内で、シンプルさを優先し、Flag構造体にIsSet boolフィールドを追加する案が採用された。フィールド名を命令形のSetではなくIsSetにすることで、将来(*Flag).Set(value string)のような設定用メソッドを追加する余地を残した。
  • 提案者のearthboundkidは「同じFlagが複数のFlagSetに属し得る」ケースを懸念しIsSet(string) boolという別APIを提案したが、aclementsが「FlagFlagSetに後から追加する手段がないため、単一のFlagSetにしか属し得ない」と指摘し、この懸念は解消された。
  • 類似の重複proposal(#80026、adonovan提案)が別途proposal evaluation boardに乗っていたことが判明し、プロセスの一貫性・透明性の欠如についてadonovan自身が言及する一幕もあった。最終的に本issue(#65675)の方でAPI設計がまとまり、likely acceptとなった。

関連リンク