[インデックス 15300] ファイルの概要
このコミットは、Go言語の型システムにおけるポインタ型のデリファレンス(間接参照)に関する修正です。具体的には、名前付きポインタ型(type MyPtr *int
のような定義)が正しくデリファレンスできるように、型チェックロジックが改善されました。これにより、Goコンパイラの型チェッカーが、名前付きポインタ型の基底型がポインタである場合に、そのデリファレンスを許可するようになります。
コミット
commit 68ff170ebece48b7fbef3c14c1514811a4d6c370
Author: Andrew Wilkins <axwalk@gmail.com>
Date: Mon Feb 18 19:03:10 2013 -0800
go/types: Permit dereferencing of named pointer types.
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/7358044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/68ff170ebece48b7fbef3c14c1514811a4d6c370
元コミット内容
diff --git a/src/pkg/go/types/expr.go b/src/pkg/go/types/expr.go
index 696a0cae68..0caa90a1d3 100644
--- a/src/pkg/go/types/expr.go
+++ b/src/pkg/go/types/expr.go
@@ -1193,7 +1193,7 @@ func (check *checker) rawExpr(x *operand, e ast.Expr, hint Type, iota int, cycle
case typexpr:
x.typ = &Pointer{Base: x.typ}
default:
- if typ, ok := x.typ.(*Pointer); ok {
+ if typ, ok := underlying(x.typ).(*Pointer); ok {
x.mode = variable
x.typ = typ.Base
} else {
diff --git a/src/pkg/go/types/testdata/expr0.src b/src/pkg/go/types/testdata/expr0.src
index c3233d36fe..8d057f63c1 100644
--- a/src/pkg/go/types/testdata/expr0.src
+++ b/src/pkg/go/types/testdata/expr0.src
@@ -149,3 +149,13 @@ var (
_ = &((((T{1, 2}))))
_ = &f /* ERROR "cannot take address" */ ()\n )
+\n+// recursive pointer types
+type P *P
+\n+var (\n+\tp1 P = new(P)\n+\tp2 P = *p1
+\tp3 P = &p2
+)\n+\n
変更の背景
Go言語の型システムでは、既存の型に新しい名前を付けて「名前付き型」を定義できます。例えば、type MyInt int
や type MyPtr *int
のように定義します。ポインタ型の場合、*T
のように直接ポインタ型を定義することもできますが、type MyPtr *int
のように名前を付けることも可能です。
このコミット以前のGoコンパイラの型チェッカー(go/types
パッケージ)では、デリファレンス演算子 *
を適用する際に、式の型が直接 *Pointer
型であるかどうかのみをチェックしていました。このため、type MyPtr *int
のように定義された「名前付きポインタ型」の変数に対してデリファレンスを行おうとすると、その名前付き型自体は *Pointer
型ではないため、型チェックエラーが発生してしまうという問題がありました。
この挙動は、Goの型システムの設計思想、特に「基底型(underlying type)」の概念と矛盾していました。Goでは、名前付き型はその基底型と同じ操作をサポートすべきであるという原則があります。したがって、名前付きポインタ型も、その基底型がポインタ型であるならば、デリファレンス可能であるべきでした。このコミットは、この不整合を解消し、名前付きポインタ型が期待通りに機能するように修正することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念が重要です。
- 型システム (Type System): Goは静的型付け言語であり、すべての変数には型があります。型はコンパイル時に決定され、プログラムの安全性と予測可能性を保証します。
- ポインタ型 (Pointer Types): Goにおけるポインタは、メモリ上の特定のアドレスを指し示す変数です。
*T
の形式で宣言され、T
型の値へのポインタであることを示します。&
演算子: 変数のアドレスを取得します(例:&x
は変数x
のアドレス)。*
演算子: ポインタが指すアドレスに格納されている値を取得します(デリファレンス、間接参照)。
- 名前付き型 (Named Types):
type NewType OldType
の形式で、既存の型に新しい名前を付けることができます。NewType
はOldType
とは異なる新しい型とみなされますが、OldType
を「基底型(underlying type)」として持ちます。- 例:
type MyInt int
の場合、MyInt
の基底型はint
です。 - 例:
type MyPtr *int
の場合、MyPtr
の基底型は*int
です。
- 例:
- 基底型 (Underlying Type): Goの型システムにおける重要な概念で、名前付き型が最終的にどのような組み込み型や複合型に基づいているかを示します。型変換や操作の互換性を判断する際に用いられます。例えば、
type A int
とtype B int
は異なる型ですが、どちらも基底型はint
です。 go/types
パッケージ: Goコンパイラの一部であり、Goプログラムの静的セマンティック分析(型チェック、スコープ解決など)を行うためのライブラリです。コンパイラがコードの正当性を検証する上で中心的な役割を担います。このパッケージは、AST (Abstract Syntax Tree) を入力として受け取り、各式の型や値、変数のスコープなどを決定します。
技術的詳細
このコミットの核心は、go/types
パッケージ内の型チェックロジック、特にデリファレンス演算子 *
の処理方法の変更にあります。
Go言語の仕様では、デリファレンス演算子 *
は、ポインタ型である式 x
に対して適用され、そのポインタが指す変数を示します。ここで重要なのは、「ポインタ型である」という条件の解釈です。
修正前の src/pkg/go/types/expr.go
の rawExpr
関数(式を評価する部分)では、デリファレンス対象の式の型 x.typ
が直接 *Pointer
型(Goの内部表現におけるポインタ型)であるかどうかを x.typ.(*Pointer)
という型アサーションで確認していました。
しかし、type MyPtr *int
のように定義された名前付きポインタ型の場合、MyPtr
自体は *Pointer
型ではありません。MyPtr
は MyPtr
という独自の型であり、その「基底型」が *int
(つまりポインタ型)なのです。したがって、修正前のロジックでは、MyPtr
型の変数 p
に対して *p
と記述すると、x.typ.(*Pointer)
のアサーションが失敗し、デリファレンスが許可されませんでした。
この問題を解決するため、変更後のコードでは underlying(x.typ).(*Pointer)
を使用しています。
underlying(x.typ)
は、x.typ
の基底型を取得する関数(または概念)です。
- もし
x.typ
が*int
のような直接的なポインタ型であれば、underlying(x.typ)
はそのまま*int
を返します。 - もし
x.typ
がMyPtr
のような名前付きポインタ型(基底型が*int
)であれば、underlying(x.typ)
は*int
を返します。
このように、まず基底型を取得してからそれがポインタ型であるかをチェックすることで、名前付きポインタ型であっても、その基底型がポインタであれば正しくデリファレンスが許可されるようになりました。これにより、Goの型システムの整合性が保たれ、より直感的なコード記述が可能になります。
コアとなるコードの変更箇所
変更は src/pkg/go/types/expr.go
ファイルの1箇所です。
--- a/src/pkg/go/types/expr.go
+++ b/src/pkg/go/types/expr.go
@@ -1193,7 +1193,7 @@ func (check *checker) rawExpr(x *operand, e ast.Expr, hint Type, iota int, cycle
case typexpr:
x.typ = &Pointer{Base: x.typ}
default:
- if typ, ok := x.typ.(*Pointer); ok {
+ if typ, ok := underlying(x.typ).(*Pointer); ok {
x.mode = variable
x.typ = typ.Base
} else {
また、この変更を検証するためのテストケースが src/pkg/go/types/testdata/expr0.src
に追加されています。
--- a/src/pkg/go/types/testdata/expr0.src
+++ b/src/pkg/go/types/testdata/expr0.src
@@ -149,3 +149,13 @@ var (\n _ = &((((T{1, 2}))))\n _ = &f /* ERROR "cannot take address" */ ()\n )
+\n+// recursive pointer types
+type P *P
+\n+var (\n+\tp1 P = new(P)\n+\tp2 P = *p1
+\tp3 P = &p2
+)\n+\n
コアとなるコードの解説
src/pkg/go/types/expr.go
の変更は、rawExpr
関数内のデリファレンス処理の一部です。
元のコード:
if typ, ok := x.typ.(*Pointer); ok {
変更後のコード:
if typ, ok := underlying(x.typ).(*Pointer); ok {
x
: 現在評価中の式を表すoperand
構造体。x.typ
はその式の型を示します。(*Pointer)
: これはGoの型アサーション構文です。x.typ
またはunderlying(x.typ)
が*Pointer
型(Goの内部でポインタ型を表す構造体)に変換可能であるかをチェックします。変換可能であればtyp
にその値が代入され、ok
はtrue
になります。underlying(x.typ)
: この関数呼び出しが変更の肝です。x.typ
が名前付き型である場合、この関数はその名前付き型の基底型を返します。例えば、x.typ
がtype P *P
で定義されたP
型であれば、underlying(x.typ)
は*P
型を返します。もしx.typ
がすでに基底型(例:*int
)であれば、underlying
はその型自身を返します。
この変更により、型チェッカーはデリファレンスを行う際に、式の直接の型だけでなく、その型の基底型がポインタ型であるかどうかも考慮するようになりました。これにより、type P *P
のような再帰的な名前付きポインタ型や、その他の名前付きポインタ型が正しくデリファレンスできるようになります。
追加されたテストケース src/pkg/go/types/testdata/expr0.src
は、この修正が意図通りに機能することを確認します。特に、type P *P
という再帰的な名前付きポインタ型を定義し、p2 P = *p1
のようにデリファレンスを行うコードが含まれています。この行がエラーなくコンパイルできることが、修正の成功を示します。
関連リンク
- Go言語の仕様 - 型 (Types): https://go.dev/ref/spec#Types
- Go言語の仕様 - ポインタ型 (Pointer types): https://go.dev/ref/spec#Pointer_types
- Go言語の仕様 - 型アサーション (Type assertions): https://go.dev/ref/spec#Type_assertions
- Go言語の仕様 - 基底型 (Underlying types): https://go.dev/ref/spec#Underlying_types
- Go言語の
go/types
パッケージのドキュメント: https://pkg.go.dev/go/types
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- Go言語のIssueトラッカーやコードレビューシステム(CL: Change List)
- このコミットのCL: https://golang.org/cl/7358044 (コミットメッセージに記載)