[インデックス 15854] ファイルの概要
このコミットは、Goコンパイラ(gc
)において「メソッド値(method values)」の概念を実装したものです。メソッド値とは、Go言語において構造体やインターフェースのメソッドを、そのレシーバにバインドされた関数として扱う機能です。これにより、myObject.MyMethod
のようにメソッドを呼び出すのではなく、f := myObject.MyMethod
のようにメソッドを関数として変数に代入し、後でf()
のように呼び出すことが可能になります。
コミット
commit d3c758d7d2a29f86d4d75bc29363532c6b8c49b0
Author: Russ Cox <rsc@golang.org>
Date: Wed Mar 20 17:11:09 2013 -0400
cmd/gc: implement method values
R=ken2, ken
CC=golang-dev
https://golang.org/cl/7546052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d3c758d7d2a29f86d4d75bc29363532c6b8c49b0
元コミット内容
cmd/gc: implement method values
R=ken2, ken
CC=golang-dev
https://golang.org/cl/7546052
変更の背景
Go言語では、メソッドは特定の型に関連付けられた関数です。これまでのGoコンパイラでは、メソッドを直接呼び出すことはできましたが、メソッドを「値」として取得し、後で呼び出すという機能はサポートされていませんでした。この機能は、関数型プログラミングのパラダイムをGoに取り入れる上で重要であり、特にコールバック関数やイベントハンドラとしてメソッドを渡す際に非常に便利です。
例えば、type MyStruct struct { Value int }
と func (m MyStruct) GetValue() int { return m.Value }
という定義があった場合、これまでは myInstance.GetValue()
のように直接呼び出す必要がありました。しかし、メソッド値が導入されることで、f := myInstance.GetValue
のようにメソッドを関数として変数 f
に代入し、その後 f()
のように呼び出すことが可能になります。これにより、コードの柔軟性と再利用性が向上します。
この変更は、Go言語の表現力を高め、より柔軟なプログラミングスタイルを可能にするための重要なステップでした。特に、インターフェースと組み合わせることで、ポリモーフィックな振る舞いをより簡潔に記述できるようになります。
前提知識の解説
Go言語のメソッド
Go言語のメソッドは、特定の型に関連付けられた関数です。メソッドはレシーバ(receiver)と呼ばれる特別な引数を持ち、これによりその型の値またはポインタに対して操作を行うことができます。
例:
type MyInt int
func (m MyInt) Add(x int) MyInt {
return m + MyInt(x)
}
func (m *MyInt) Increment() {
*m++
}
Add
メソッドは値レシーバを持ち、Increment
メソッドはポインタレシーバを持ちます。
クロージャ (Closures)
クロージャとは、関数が定義された環境(レキシカルスコープ)を記憶し、その環境内の変数を参照・操作できる関数のことです。Go言語では、関数リテラル(無名関数)がクロージャとして機能します。
例:
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
a := adder()
fmt.Println(a(1)) // 1
fmt.Println(a(2)) // 3
この例では、adder
関数が返す無名関数が、sum
変数を「記憶」しており、adder
の呼び出しが終わった後も sum
の値を更新し続けることができます。
Goコンパイラ (gc
) の内部構造
Goコンパイラ gc
は、Goのソースコードを機械語に変換する役割を担っています。その過程で、以下の主要なフェーズがあります。
- 字句解析 (Lexing): ソースコードをトークンに分割します。
- 構文解析 (Parsing): トークン列から抽象構文木(AST)を構築します。
- 型チェック (Type Checking): ASTの各ノードの型を解決し、型の一貫性を検証します。このフェーズで、メソッド解決やインターフェース適合性のチェックが行われます。
- 中間表現 (IR) 生成: ASTを、より機械語に近い中間表現に変換します。
- 最適化 (Optimization): 中間表現に対して様々な最適化を適用します。
- コード生成 (Code Generation): 最適化された中間表現から最終的な機械語コードを生成します。
このコミットでは、主に型チェック (typecheck.c
)、中間表現の変換 (walk.c
)、およびコード生成 (gsubr.c
, gen.c
, closure.c
) の部分に影響を与えています。特に、メソッド値をクロージャとして表現し、そのための新しいノードタイプや処理ロジックが追加されています。
ポインタとnil参照
Go言語では、ポインタはメモリ上のアドレスを指します。nilポインタは、何も指していない状態を表します。nilポインタをデリファレンス(参照解除)しようとすると、ランタイムパニックが発生します。メソッド値の導入にあたり、レシーバがnilの場合の挙動(特にポインタレシーバを持つメソッドの場合)が考慮される必要があります。
技術的詳細
このコミットの核心は、Goコンパイラがメソッド値をどのように内部的に表現し、処理するかという点にあります。Goのメソッド値は、レシーバがバインドされたクロージャとして実装されます。
具体的には、obj.Method
のようなメソッド値の式は、コンパイル時に以下のような構造を持つ新しい関数(クロージャ)に変換されます。
- レシーバのキャプチャ: メソッド値が作成される際、元のレシーバ(
obj
)がクロージャの環境内にキャプチャされます。これにより、生成された関数が呼び出されたときに、元のレシーバにアクセスできます。 - 新しい関数定義の生成: コンパイラは、元のメソッドのシグネチャにレシーバを含まない新しい関数を内部的に生成します。この新しい関数は、キャプチャされたレシーバを使って元のメソッドを呼び出します。
OCALLPART
ノードの導入:go.h
にOCALLPART
という新しいノードタイプが追加されています。これは、括弧なしでメソッドが参照された場合(つまりメソッド値として扱われる場合)を表します。OCLOSUREVAR
の利用: クロージャ内でキャプチャされた変数を参照するためにOCLOSUREVAR
ノードが利用されます。これは、クロージャの環境から変数をロードするための特別なノードです。makepartialcall
関数の追加:src/cmd/gc/closure.c
にmakepartialcall
という新しい関数が追加されています。この関数は、メソッド値に対応する内部的な関数(クロージャ)を構築する役割を担います。この関数は、レシーバをキャプチャし、元のメソッドを呼び出すためのボディを持つ新しい関数ノードを作成します。walkpartialcall
関数の追加:src/cmd/gc/walk.c
にwalkpartialcall
が追加され、OCALLPART
ノードのウォーク処理を行います。この関数は、メソッド値をコンポジットリテラル(複合リテラル)として表現し、その中にメソッドの関数ポインタとレシーバの値を格納します。- nilポインタチェックの強化:
checkref
関数にforce
引数が追加され、nilポインタデリファレンスチェックの挙動がより細かく制御できるようになっています。特に、メソッド値がnilレシーバから作成される場合のパニック挙動がtest/method5.go
で詳細にテストされています。値レシーバのメソッドをnilポインタから取得しようとするとパニックしますが、ポインタレシーバのメソッドをnilポインタから取得する場合はパニックしないというGoの仕様が反映されています。
この実装により、Goのコンパイラは、メソッド呼び出しとメソッド値の取得を区別し、それぞれを適切な内部表現に変換できるようになりました。メソッド値は、実行時にはレシーバがバインドされた通常の関数として扱われるため、Goのランタイムは特別な変更を必要としません。
コアとなるコードの変更箇所
このコミットで最も重要な変更は、src/cmd/gc/closure.c
と src/cmd/gc/typecheck.c
、src/cmd/gc/walk.c
に集中しています。
src/cmd/gc/closure.c
:makepartialcall
関数が追加されました。この関数は、メソッド値に対応する新しい関数(クロージャ)を生成します。この生成された関数は、レシーバを引数として受け取り、元のメソッドを呼び出すロジックを含みます。walkcallclosure
関数が削除され、より汎用的なmakepartialcall
とwalkpartialcall
に置き換えられました。
src/cmd/gc/typecheck.c
:typecheckpartialcall
関数が追加されました。これは、ODOTMETH
やODOTINTER
ノードが呼び出しではなくメソッド値として使用される場合に、OCALLPART
ノードに変換する役割を担います。reswitch
ラベル内のODOTINTER
およびODOTMETH
の処理が変更され、top&Ecall
フラグに基づいてtypecheckpartialcall
が呼び出されるようになりました。
src/cmd/gc/walk.c
:walkpartialcall
関数が追加されました。この関数は、OCALLPART
ノードを処理し、レシーバとメソッドの関数ポインタを含む複合リテラル(クロージャ)を生成します。OCLOSURE
ノードのウォーク処理が変更され、walkclosure
が呼び出されるようになりました。ODOTPTR
およびOIND
のウォーク処理において、nilポインタチェック (checknotnil
) が追加されました。
src/cmd/gc/go.h
:- 新しいノードタイプ
OCALLPART
とOCHECKNOTNIL
が追加されました。 typecheckpartialcall
とwalkpartialcall
の関数プロトタイプが追加されました。checkref
関数のシグネチャが変更され、force
引数が追加されました。
- 新しいノードタイプ
src/cmd/gc/gen.c
:OCHECKNOTNIL
ノードのコード生成ロジックが追加されました。OSLICEARR
のcheckref
呼び出しにforce
引数が渡されるようになりました。
src/cmd/gc/subr.c
:checknotnil
関数が追加されました。これは、nilポインタチェックを挿入するためのヘルパー関数です。
src/cmd/5g/gsubr.c
,src/cmd/6g/gsubr.c
,src/cmd/8g/gsubr.c
:ismem
関数にOCLOSUREVAR
が追加され、クロージャ変数がメモリ上の値として扱われるようになりました。checkref
関数のシグネチャが変更され、force
引数が追加されました。naddr
関数内のOCLOSUREVAR
の処理が簡素化され、レジスタへの移動命令が削除されました。これは、OCLOSUREVAR
が直接アドレスとして扱えるようになったためです。
test/method5.go
:- メソッド値の挙動を検証するための新しいテストファイルが追加されました。様々なレシーバ型(バイト、ワード、ビッグ、値、ポインタ)と、エクスポートされた/されていないメソッドの両方について、メソッド値の取得と呼び出しが正しく機能するかをテストしています。また、nilレシーバからのメソッド値取得時のパニック挙動も検証しています。
コアとなるコードの解説
src/cmd/gc/closure.c
の makepartialcall
この関数は、メソッド値 X.M
が評価される際に、コンパイラが内部的に生成する「バインドされたメソッド関数」を作成します。
static Node*
makepartialcall(Node *fn, Type *t0, Node *meth)
{
Node *ptr, *n, *call, *xtype, *xfunc, *cv;
Type *rcvrtype, *basetype, *t;
NodeList *body, *l, *callargs, *retargs;
char *p;
Sym *sym;
int i;
// ... (シンボル名の生成など) ...
// 新しい関数の型 (xtype) を構築
xtype = nod(OTFUNC, N, N);
i = 0;
l = nil;
callargs = nil;
xfunc = nod(ODCLFUNC, N, N); // 新しい関数ノード
// 元のメソッドの引数を新しい関数の引数としてコピー
for(t = getinargx(t0)->type; t; t = t->down) {
snprint(namebuf, sizeof namebuf, "a%d", i++);
n = newname(lookup(namebuf));
n->class = PPARAM;
xfunc->dcl = list(xfunc->dcl, n);
callargs = list(callargs, n);
l = list(l, nod(ODCLFIELD, n, typenod(t->type)));
}
xtype->list = l; // 引数リスト
// ... (戻り値の型を構築) ...
xfunc->dupok = 1;
xfunc->nname = newname(sym);
xfunc->nname->sym->flags |= SymExported; // disable export
xfunc->nname->ntype = xtype;
xfunc->nname->defn = xfunc;
declare(xfunc->nname, PFUNC);
// レシーバ変数を宣言し、初期化
body = nil;
cv = nod(OCLOSUREVAR, N, N); // クロージャ変数ノード
cv->xoffset = widthptr; // オフセット (レシーバが格納される場所)
cv->type = rcvrtype; // レシーバの型
ptr = nod(ONAME, N, N); // レシーバを表す名前ノード
ptr->sym = lookup("rcvr");
ptr->class = PAUTO; // 自動変数
// ... (レシーバの初期化: 値レシーバならアドレス、ポインタレシーバなら直接) ...
if(isptr[rcvrtype->etype] || isinter(rcvrtype)) {
ptr->ntype = typenod(rcvrtype);
body = list(body, nod(OAS, ptr, cv)); // ptr = cv
} else {
ptr->ntype = typenod(ptrto(rcvrtype));
body = list(body, nod(OAS, ptr, nod(OADDR, cv, N))); // ptr = &cv
}
// 元のメソッドを呼び出す (call = ptr.meth(callargs))
call = nod(OCALL, nod(OXDOT, ptr, meth), N);
call->list = callargs;
if(t0->outtuple == 0) { // 戻り値がない場合
body = list(body, call);
} else { // 戻り値がある場合
n = nod(OAS2, N, N); // 複数代入ノード
n->list = retargs; // 戻り値変数リスト
n->rlist = list1(call); // 呼び出し結果
body = list(body, n);
n = nod(ORETURN, N, N); // return
body = list(body, n);
}
xfunc->nbody = body; // 新しい関数のボディを設定
typecheck(&xfunc, Etop); // 型チェック
sym->def = xfunc;
xtop = list(xtop, xfunc); // トップレベルの関数リストに追加
return xfunc; // 生成された関数ノードを返す
}
この関数は、X.M
のようなメソッド値が参照されたときに、コンパイラが内部的に生成する新しい関数(クロージャ)を構築します。この新しい関数は、元のレシーバ X
をクロージャ変数 cv
としてキャプチャし、その cv
を使って元のメソッド M
を呼び出すように実装されます。これにより、メソッド値はレシーバがバインドされた通常の関数として振る舞います。
src/cmd/gc/typecheck.c
の typecheckpartialcall
この関数は、型チェックフェーズでメソッド値の式を検出した際に呼び出されます。
void
typecheckpartialcall(Node *fn, Node *sym)
{
switch(fn->op) {
case ODOTINTER: // インターフェースメソッド
case ODOTMETH: // 構造体メソッド
break;
default:
fatal("invalid typecheckpartialcall");
}
// トップレベル関数を作成 (makepartialcall を呼び出す)
fn->nname = makepartialcall(fn, fn->type, sym);
fn->op = OCALLPART; // ノードタイプを OCALLPART に変更
fn->type = fn->right->type; // 型をメソッドの関数型に設定
}
typecheckpartialcall
は、obj.Method
のような式が、メソッド呼び出しではなくメソッド値として扱われるべきだと判断された場合に、そのASTノードのオペレーションを OCALLPART
に変更し、makepartialcall
を呼び出して対応する内部関数を生成します。
src/cmd/gc/walk.c
の walkpartialcall
この関数は、OCALLPART
ノードをウォークする際に呼び出され、最終的なクロージャの表現を構築します。
Node*
walkpartialcall(Node *n, NodeList **init)
{
Node *clos, *typ;
// インターフェースメソッドの場合、レシーバを評価しnilチェック
if(isinter(n->left->type)) {
n->left = cheapexpr(n->left, init);
checknotnil(n->left, init);
}
// クロージャを複合リテラルとして作成
// struct{F uintptr; R T}{M.T·f, x} のような形
typ = nod(OTSTRUCT, N, N);
// F: 関数ポインタ
typ->list = list1(nod(ODCLFIELD, newname(lookup("F")), typenod(types[TUINTPTR])));
// R: レシーバ
typ->list = list(typ->list, nod(ODCLFIELD, newname(lookup("R")), typenod(n->left->type)));
clos = nod(OCOMPLIT, N, nod(OIND, typ, N)); // 複合リテラルノード
clos->esc = n->esc;
clos->right->implicit = 1;
// F の値: 生成された関数のポインタ
clos->list = list1(nod(OCFUNC, n->nname->nname, N));
// R の値: 元のレシーバ
clos->list = list(clos->list, n->left);
// *struct から func 型への強制型変換
clos = nod(OCONVNOP, clos, N);
clos->type = n->type;
typecheck(&clos, Erv);
// typecheck will insert a PTRLIT node under CONVNOP,
// tag it with escape analysis result.
clos->left->esc = n->esc;
walkexpr(&clos, init);
return clos;
}
walkpartialcall
は、typecheckpartialcall
で生成された OCALLPART
ノードを受け取り、それを実際のクロージャオブジェクトに変換します。このクロージャオブジェクトは、内部的には2つのフィールドを持つ構造体として表現されます。1つはメソッドの実体へのポインタ(関数ポインタ)、もう1つはバインドされたレシーバの値です。この構造体は、最終的にメソッド値の関数型に変換されます。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/
- Go言語のメソッドに関する公式ブログ記事: https://blog.golang.org/methods
- Go言語のクロージャに関する公式ブログ記事: https://blog.golang.org/closures
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gc
ディレクトリ) - Go言語のコンパイラに関する一般的な情報源
- Go言語のメソッド値に関する議論やドキュメント (コミット当時の情報を含む)
- Go言語のクロージャの実装に関する情報