#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 名に使わないための変更と考えられます。