メインコンテンツへスキップ
#60994active

go/types: add func CommonUnderlying(Type) Type, (formerly CoreType)

新規提案

要約

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

概要

go/types パッケージに、型パラメータの制約に含まれる全ての型が共有する「共通の基底型(underlying type)」を返す関数 CommonUnderlying(t Type) Type を追加する提案です。もとは仕様上の概念「コア型(core type)」を返す CoreType を公開する提案でしたが、名前と仕様を整理し直して再提出されました。

ステータス変更

(新規) → active
この提案は2023年に出され、2025年4月にいったん「obsolete(不要になった)」としてクローズされていました。言語仕様からコア型の概念を取り除く #70128 が採択されたため、この API も要らなくなると考えられたからです。
2026年10月22日、@adonovan が議論を再開しました。理由は次のとおりです。

  • 仕様の文言からコア型は消えたが、x/tools には今も CoreType の呼び出しが52箇所ある。
  • 調べた限り、どの呼び出しも正しく動いている。
  • 型集合を扱う TypeSet API(#61013)に置き換えたほうが適切な箇所は1つもない。
    コア型は仕様上の名前を失っても、ツール開発では実際に必要な概念として残っています。そこで名前を CommonUnderlying に変え、仕様を書き直したうえで進めることになりました。10月28日のレビューでは @adonovan と @griesemer が詳細を詰めることになり、10月29日に active 列に加わりました。今後は週次の proposal review meeting で正式に審議されます。

技術的背景

現状の問題点

Go 1.18 でジェネリクスが入ると同時にコア型の考え方が導入されました。ところが go/types は内部関数 coreType() を非公開のまま持つだけでした。そのため同じロジックが何度も別々に実装されています。代表例は次の3つです。

  • x/tools の internal/typeparams.CoreType
  • GopherJS
  • pkg.go.dev 上のさまざまなパッケージ
    型集合の扱いは細かい規則が多く、チャネルの方向の扱いなどが典型例です。各所でコピーを持ち続けると、保守の負担になり、実装ごとに挙動がずれる危険もあります。

提案された解決策

package types
// CommonUnderlying は t が表す型集合に含まれる型の共通の基底型を返す。
// そのような型がなければ nil を返す。
func CommonUnderlying(t Type) Type
  • t が型パラメータでない場合: t.Underlying() をそのまま返します。インターフェース型も同じ扱いで、インターフェースの型集合ではなく、その型自身を1つの型として扱います。
  • t が型パラメータの場合: 制約の型集合に含まれる全ての具体的な型(specific type)が同じ基底型 U を持っていれば U を返します。
  • nil を返す場合: 具体的な型がない場合(any や comparable など)や、基底型がそろわない場合です。
  • チャネルの特例: 要素型が全て同じで、方向が矛盾しないチャネル型ばかりなら、最も制限の強いチャネル型を返します。例えば chan int と <-chan int が混ざっていれば <-chan int になります。

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

  • 静的解析ツール(analyzer など): 型パラメータ T に対して「ポインタとして扱えるか」「スライスとして扱えるか」を、標準 API だけで判定できます。
  • 代替コンパイラやトランスパイラ(GopherJS など): ジェネリックなコードで range、インデックス、チャネル操作などをどう変換するか決める際に使えます。
  • コード生成ツールやリファクタリングツール: 自前で型集合を計算するロジックを捨て、go/types の実装と常に一致する結果を得られます。

コード例

// Before: x/tools の internal パッケージを使うか、自前で再実装する必要があった
func elemOfPointer(t types.Type) types.Type {
    if tp, ok := t.(*types.TypeParam); ok {
        // 制約インターフェースの項を自前で走査し、基底型の一致を確認する...
        _ = tp
        return nil
    }
    if ptr, ok := t.Underlying().(*types.Pointer); ok {
        return ptr.Elem()
    }
    return nil
}
// After: 標準 API の結果に型アサーションをかけるだけで済む
func elemOfPointer(t types.Type) types.Type {
    if ptr, ok := types.CommonUnderlying(t).(*types.Pointer); ok {
        return ptr.Elem()
    }
    return nil
}

@adonovan によると、x/tools での使い方もほとんどがこの「呼び出した直後に型アサーションや型 switch をかける」形です。

議論のハイライト

  • API 範囲の検討: @findleyr は当初、解析ツールには型集合を正規化した形を返す API も必要だとして、コア型 API と一緒に出すことを提案しました。最終的にはそちらを #61013(TypeSet API)に切り出し、この提案は単独で決められるようにしました。
  • nil を返すことへの対応: Underlying と違って nil を返しうる点について、(Type, bool) を返して呼び出し側に確認を強制する案が出ました。@adonovan は不要寄りの意見で、@timothy-king も「bool は nil と意味が重なる」と指摘しました。そのため、ドキュメントで nil の場合をはっきり書く方向になっています。
  • bytestring の扱い: 旧仕様では []byte | string に特殊な「bytestring」というコア型がありました。これをどう表すかが論点として挙がりました。新しい仕様案には bytestring の特例が含まれていません。これは、仕様からコア型が消えた流れに合わせたものと考えられます。
  • いったんクローズした後の再開: #70128 でコア型が仕様から消えたため2025年に一度閉じられました。しかし実際のツールでは今も必要とされていることがわかり、名前を CommonUnderlying に変えて再開されました。この名前は、仕様にもう存在しない用語を API 名に使わないための変更と考えられます。

関連リンク