Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 のようなメソッド値の式は、コンパイル時に以下のような構造を持つ新しい関数(クロージャ)に変換されます。

  1. レシーバのキャプチャ: メソッド値が作成される際、元のレシーバ(obj)がクロージャの環境内にキャプチャされます。これにより、生成された関数が呼び出されたときに、元のレシーバにアクセスできます。
  2. 新しい関数定義の生成: コンパイラは、元のメソッドのシグネチャにレシーバを含まない新しい関数を内部的に生成します。この新しい関数は、キャプチャされたレシーバを使って元のメソッドを呼び出します。
  3. OCALLPART ノードの導入: go.hOCALLPART という新しいノードタイプが追加されています。これは、括弧なしでメソッドが参照された場合(つまりメソッド値として扱われる場合)を表します。
  4. OCLOSUREVAR の利用: クロージャ内でキャプチャされた変数を参照するために OCLOSUREVAR ノードが利用されます。これは、クロージャの環境から変数をロードするための特別なノードです。
  5. makepartialcall 関数の追加: src/cmd/gc/closure.cmakepartialcall という新しい関数が追加されています。この関数は、メソッド値に対応する内部的な関数(クロージャ)を構築する役割を担います。この関数は、レシーバをキャプチャし、元のメソッドを呼び出すためのボディを持つ新しい関数ノードを作成します。
  6. walkpartialcall 関数の追加: src/cmd/gc/walk.cwalkpartialcall が追加され、OCALLPART ノードのウォーク処理を行います。この関数は、メソッド値をコンポジットリテラル(複合リテラル)として表現し、その中にメソッドの関数ポインタとレシーバの値を格納します。
  7. nilポインタチェックの強化: checkref 関数に force 引数が追加され、nilポインタデリファレンスチェックの挙動がより細かく制御できるようになっています。特に、メソッド値がnilレシーバから作成される場合のパニック挙動が test/method5.go で詳細にテストされています。値レシーバのメソッドをnilポインタから取得しようとするとパニックしますが、ポインタレシーバのメソッドをnilポインタから取得する場合はパニックしないというGoの仕様が反映されています。

この実装により、Goのコンパイラは、メソッド呼び出しとメソッド値の取得を区別し、それぞれを適切な内部表現に変換できるようになりました。メソッド値は、実行時にはレシーバがバインドされた通常の関数として扱われるため、Goのランタイムは特別な変更を必要としません。

コアとなるコードの変更箇所

このコミットで最も重要な変更は、src/cmd/gc/closure.csrc/cmd/gc/typecheck.csrc/cmd/gc/walk.c に集中しています。

  • src/cmd/gc/closure.c:
    • makepartialcall 関数が追加されました。この関数は、メソッド値に対応する新しい関数(クロージャ)を生成します。この生成された関数は、レシーバを引数として受け取り、元のメソッドを呼び出すロジックを含みます。
    • walkcallclosure 関数が削除され、より汎用的な makepartialcallwalkpartialcall に置き換えられました。
  • src/cmd/gc/typecheck.c:
    • typecheckpartialcall 関数が追加されました。これは、ODOTMETHODOTINTER ノードが呼び出しではなくメソッド値として使用される場合に、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:
    • 新しいノードタイプ OCALLPARTOCHECKNOTNIL が追加されました。
    • typecheckpartialcallwalkpartialcall の関数プロトタイプが追加されました。
    • checkref 関数のシグネチャが変更され、force 引数が追加されました。
  • src/cmd/gc/gen.c:
    • OCHECKNOTNIL ノードのコード生成ロジックが追加されました。
    • OSLICEARRcheckref 呼び出しに 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.cmakepartialcall

この関数は、メソッド値 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.ctypecheckpartialcall

この関数は、型チェックフェーズでメソッド値の式を検出した際に呼び出されます。

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.cwalkpartialcall

この関数は、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言語のソースコード (特に src/cmd/gc ディレクトリ)
  • Go言語のコンパイラに関する一般的な情報源
  • Go言語のメソッド値に関する議論やドキュメント (コミット当時の情報を含む)
  • Go言語のクロージャの実装に関する情報