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

[インデックス 1574] ファイルの概要

このコミットは、Go言語の初期段階におけるコンパイラ (6g, gc) と標準ライブラリ (fmt) の内部的な変更に関するものです。特に、可変引数 (...) の実装に関連するインターフェースの扱いが、ポインタから構造体へと変更されています。これにより、可変引数の処理がより堅牢になり、defer 文との連携も改善されています。

コミット

commit 743ac07cc3c7761ecd808208911b106551a7ba4f
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jan 27 15:05:25 2009 -0800

    change dotdotdot interfaces to be structs,
    not pointers to structs.
    
    fix defered dotdotdot.
    
    R=r,ken
    DELTA=25  (7 added, 5 deleted, 13 changed)
    OCL=23620
    CL=23625

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/743ac07cc3c7761ecd808208911b106551a7ba4f

元コミット内容

このコミットの元のメッセージは以下の通りです。

"change dotdotdot interfaces to be structs, not pointers to structs. fix defered dotdotdot."

これは、可変引数(...)を扱う内部的なインターフェースの表現を、ポインタから直接構造体に変更し、それによって defer 文で可変引数を使用した場合の不具合を修正したことを示しています。

変更の背景

Go言語の初期設計において、可変引数(variadic functions)は ... 構文で表現され、内部的には特定のインターフェース型として扱われていました。このコミット以前は、この「dotdotdotインターフェース」がポインタ型 (reflect.PtrValue) として表現されていたと考えられます。

しかし、ポインタとして扱うことにはいくつかの問題がありました。

  1. メモリ管理とガベージコレクションの複雑性: ポインタは間接参照を伴うため、メモリ管理やガベージコレクションのオーバーヘッドが増加する可能性があります。
  2. defer 文との相互作用: defer 文は関数の実行が終了する直前に呼び出される関数を登録しますが、可変引数がポインタとして渡される場合、defer された関数が実行されるまでに引数の値が変更されてしまう可能性がありました。これは、ポインタが指す先のデータが変更されると、defer された関数が意図しない古い値ではなく、変更後の新しい値を参照してしまうという問題を引き起こします。コミットメッセージにある "fix defered dotdotdot" は、この問題を指していると推測されます。
  3. 型の安全性とシンプルさ: ポインタを介したアクセスは、直接構造体を扱うよりも型の安全性が低下し、コードの複雑性を増す可能性があります。

これらの問題を解決するため、可変引数を表す内部インターフェースをポインタではなく直接構造体として扱うように変更する必要がありました。これにより、値渡しに近いセマンティクスが実現され、defer 文での引数のキャプチャがより予測可能になります。

前提知識の解説

1. Go言語の可変引数 (...)

Go言語では、関数の最後の引数に ... を付けることで、その関数が任意の数の引数を受け取れるようになります。例えば func sum(nums ...int) のように定義します。関数内部では、nums はスライスとして扱われます。

2. reflect パッケージ

reflect パッケージは、Goプログラムが実行時に自身の構造を検査(リフレクション)したり、値を操作したりするための機能を提供します。特に、reflect.Value はGoのあらゆる値を抽象的に表現するための型です。

  • reflect.NewValue(a): a の値に対応する reflect.Value を返します。
  • reflect.PtrValue: reflect.Value の一種で、ポインタ型の値を表します。
  • reflect.StructValue: reflect.Value の一種で、構造体型の値を表します。
  • Sub() メソッド: reflect.PtrValue 型の Value に対して呼び出され、そのポインタが指す先の値(要素)の reflect.Value を返します。

このコミット以前は、可変引数が reflect.PtrValue として扱われ、その実体は Sub() メソッドで取得する必要があったことが示唆されます。

3. Goコンパイラの内部構造

Goコンパイラは、ソースコードを機械語に変換する過程で、抽象構文木(AST)を構築し、型チェック、最適化、コード生成などのフェーズを経ます。

  • src/cmd/6g: 6g は、amd64アーキテクチャ向けのGoコンパイラです。
  • src/cmd/gc: gc は、Goコンパイラの共通部分(ジェネリックコード)です。
  • TINTER: インターフェース型を表す内部的な型定数。
  • TDDD: 可変引数(...)を表す内部的な型定数。このコミットで TINTER と同様に扱われるようになります。
  • Node: コンパイラ内部でASTのノードを表す構造体。
  • OAS: 代入操作を表すASTノードのオペレーションコード。
  • OADDR: アドレス取得操作を表すASTノードのオペレーションコード。
  • walktype: コンパイラの walk フェーズで型を処理する関数。
  • mkdotargs: 可変引数に関連する引数を生成する関数。
  • addtop: コンパイラ内部で、トップレベルのステートメントリストに追加するための変数。
  • convas: 代入を変換する関数。

4. fmt パッケージ

fmt パッケージは、Go言語におけるフォーマット済みI/O(入出力)を提供する標準ライブラリです。Printf, Sprintf, Fprint などの関数が含まれ、これらは可変引数を受け取ります。

技術的詳細

このコミットの主要な変更点は、Goコンパイラと fmt パッケージにおいて、可変引数(...)の内部表現をポインタから構造体へと変更したことです。

コンパイラ側の変更 (src/cmd/6g/obj.c, src/cmd/gc/subr.c, src/cmd/gc/walk.c)

  1. TDDD 型の導入と TINTER との統合:

    • src/cmd/6g/obj.cdumpsignatures 関数では、型が TINTER (インターフェース) または TDDD (可変引数) の場合に、シグネチャのダンプ処理を行うように変更されました。これは、可変引数がインターフェースと同様に扱われるようになったことを示唆しています。
    • src/cmd/gc/subr.csigname 関数でも同様に、TINTER または TDDD の場合にシグネチャ名を生成するロジックが追加されています。
    • signame 関数には、strcmp(buf, "...") == 0 の場合に strcpy(buf, "dotdotdot") とする特殊なケースが追加されています。これは、アセンブリコードで sigi.... のような読みにくい表記を避けるためのものです。
  2. OPROC ノードの処理変更 (src/cmd/gc/walk.c):

    • walk.cwalk 関数内の OPROC (プロシージャ呼び出し) のケースで、walkstate(n->left)walktype(n->left, Etop) に変更されました。これは、プロシージャ呼び出しの引数処理において、状態ベースのウォークから型ベースのウォークに切り替わったことを示唆しており、より厳密な型チェックと処理が可能になったと考えられます。
  3. mkdotargs 関数の変更 (src/cmd/gc/walk.c):

    • この関数は可変引数を処理するための内部的な構造体を構築します。
    • 以前は、可変引数の実体を指すポインタを生成し、それを DDD パラメータに代入していました (a = nod(OADDR, var, N); a->type = ptrto(st); a = nod(OAS, nodarg(l, fp), a);)。
    • 変更後、OADDR を介したポインタの生成と代入が削除され、直接構造体 varDDD パラメータに代入するようになりました (a = nod(OAS, nodarg(l, fp), var);)。
    • さらに、構造体のフィールドへの代入 (*r->left = *var;) を addtop リストに追加するように変更されました (addtop = list(addtop, r);)。これは、reorder1 という最適化フェーズが、インターフェース変換の後にこれらの代入を再順序付けしないようにするためのものです。これにより、可変引数の値が正しくキャプチャされることが保証されます。

fmt パッケージ側の変更 (src/lib/fmt/print.go)

fmt パッケージの Fprintf, Printf, Sprintf, Fprint, Print, Sprint, Fprintln, Println, Sprintln といった可変引数を受け取る関数群で、引数のリフレクション処理が変更されました。

  • 変更前: v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
    • これは、可変引数 areflect.PtrValue として扱われ、そのポインタが指す先の値(Sub() で取得)が reflect.StructValue にキャストされることを意味します。
  • 変更後: v := reflect.NewValue(a).(reflect.StructValue);
    • これは、可変引数 a が直接 reflect.StructValue として扱われるようになったことを意味します。

この変更は、コンパイラ側での TDDD 型の扱いと密接に関連しています。コンパイラが可変引数をポインタではなく構造体として内部的に表現するようになったため、fmt パッケージもその新しい表現に合わせてリフレクション処理を変更したのです。これにより、fmt 関数が可変引数をより直接的かつ効率的に処理できるようになります。

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

src/cmd/6g/obj.c

--- a/src/cmd/6g/obj.c
+++ b/src/cmd/6g/obj.c
@@ -941,7 +941,7 @@ dumpsignatures(void)
 		s->siggen = 1;
 
 		// interface is easy
-		if(et == TINTER) {
+		if(et == TINTER || et == TDDD) {
 			if(t->sym && !t->local)
 				continue;
 			dumpsigi(t, s);

src/cmd/gc/subr.c

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -1609,7 +1609,7 @@ signame(Type *t)
 		goto bad;
 
 	e = "sigt";
-	if(t->etype == TINTER)
+	if(t->etype == TINTER || t->etype == TDDD)
 		e = "sigi";
 
 	// name is exported name, like *[]byte or *Struct or Interface
@@ -1620,6 +1620,10 @@ signame(Type *t)
 	// so that it can be referred to by the runtime.
 	if(strcmp(buf, "interface { }") == 0)
 		strcpy(buf, "empty");
+	
+	// special case: sigi.... is just too hard to read in assembly.
+	if(strcmp(buf, "...") == 0)
+		strcpy(buf, "dotdotdot");
 
 	ss = pkglookup(buf, e);
 	if(ss->oname == N) {

src/cmd/gc/walk.c

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -348,7 +348,7 @@ loop:
 	case OPROC:
 		if(top != Etop)
 			goto nottop;
-		walkstate(n->left);
+		walktype(n->left, Etop);
 		goto ret;
 
 	case OCALLMETH:
@@ -1820,7 +1820,10 @@ mkdotargs(Node *r, Node *rr, Iter *saver, Node *nn, Type *l, int fp)
 	var = nod(OXXX, N, N);
 	tempname(var, st);
 
-	// assign the fields to the struct
+	// assign the fields to the struct.
+	// use addtop so that reorder1 doesn't reorder
+	// these assignments after the interface conversion
+	// below.
 	n = rev(n);
 	r = listfirst(&saven, &n);
 	t = st->type;
@@ -1829,7 +1832,7 @@ mkdotargs(Node *r, Node *rr, Iter *saver, Node *nn, Type *l, int fp)
 		*r->left = *var;
 		r->left->type = r->right->type;
 		r->left->xoffset += t->width;
-		nn = list(r, nn);
+		addtop = list(addtop, r);
 		r = listnext(&saven);
 		t = t->down;
 	}
@@ -1837,13 +1840,8 @@ mkdotargs(Node *r, Node *rr, Iter *saver, Node *nn, Type *l, int fp)
 	// last thing is to put assignment
 	// of a pointer to the structure to
 	// the DDD parameter
-
-	a = nod(OADDR, var, N);
-	a->type = ptrto(st);
-	a = nod(OAS, nodarg(l, fp), a);
-	a = convas(a);
-
-	nn = list(a, nn);
+	a = nod(OAS, nodarg(l, fp), var);
+	nn = list(convas(a), nn);
 
 	return nn;
 }

src/lib/fmt/print.go

--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -130,7 +130,7 @@ func (p *pp) doprint(v reflect.StructValue, addspace, addnewline bool);
 // These routines end in 'f' and take a format string.
 
 func Fprintf(w io.Write, format string, a ...) (n int, error *os.Error) {
-	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
+	v := reflect.NewValue(a).(reflect.StructValue);
 	p := newPrinter();
 	p.doprintf(format, v);
 	n, error = w.Write(p.buf[0:p.n]);
@@ -143,7 +143,7 @@ func Printf(format string, v ...) (n int, errno *os.Error) {
 }
 
 func Sprintf(format string, a ...) string {
-	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
+	v := reflect.NewValue(a).(reflect.StructValue);
 	p := newPrinter();
 	p.doprintf(format, v);
 	s := string(p.buf)[0 : p.n];
@@ -154,7 +154,7 @@ func Sprintf(format string, a ...) string {
 // when the operand on neither side is a string.
 
 func Fprint(w io.Write, a ...) (n int, error *os.Error) {
-	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
+	v := reflect.NewValue(a).(reflect.StructValue);
 	p := newPrinter();
 	p.doprint(v, false, false);
 	n, error = w.Write(p.buf[0:p.n]);
@@ -167,7 +167,7 @@ func Print(v ...) (n int, errno *os.Error) {
 }
 
 func Sprint(a ...) string {
-	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
+	v := reflect.NewValue(a).(reflect.StructValue);
 	p := newPrinter();
 	p.doprint(v, false, false);
 	s := string(p.buf)[0 : p.n];
@@ -179,7 +179,7 @@ func Sprint(a ...) string {
 // after the last operand.
 
 func Fprintln(w io.Write, a ...) (n int, error *os.Error) {
-	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
+	v := reflect.NewValue(a).(reflect.StructValue);
 	p := newPrinter();
 	p.doprint(v, true, true);
 	n, error = w.Write(p.buf[0:p.n]);
@@ -192,7 +192,7 @@ func Println(v ...) (n int, errno *os.Error) {
 }
 
 func Sprintln(a ...) string {
-	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
+	v := reflect.NewValue(a).(reflect.StructValue);
 	p := newPrinter();
 	p.doprint(v, true, true);
 	s := string(p.buf)[0 : p.n];

コアとなるコードの解説

コンパイラ側の変更

  • src/cmd/6g/obj.csrc/cmd/gc/subr.c: これらの変更は、コンパイラが可変引数 (TDDD) をインターフェース (TINTER) と同様に、シグネチャのダンプや名前生成の際に特別に扱うようになったことを示しています。これは、可変引数がより「値」に近いセマンティクスを持つようになったことの反映です。特に signame 関数での ...dotdotdot に変更する特殊ケースは、アセンブリレベルでの可読性を向上させるための細かな配慮です。
  • src/cmd/gc/walk.cOPROC 処理: walkstate から walktype への変更は、プロシージャ呼び出しの引数処理が、より型に厳密に基づいたものになったことを示唆しています。これは、可変引数が構造体として扱われるようになったことと整合性が取れています。
  • src/cmd/gc/walk.cmkdotargs 関数: この関数の変更が最も重要です。
    • 以前は、可変引数の実体へのポインタを生成し、それを DDD パラメータに代入していました。これは、defer 文で可変引数を使用した場合に、ポインタが指す先の値が変更されると、defer された関数が意図しない値を参照してしまう問題を引き起こす可能性がありました。
    • 変更後は、ポインタを介さずに直接構造体 varDDD パラメータに代入しています。これにより、可変引数の値がその時点でのスナップショットとしてキャプチャされ、defer 文が実行される際に値が変更される心配がなくなります。
    • 構造体のフィールドへの代入を addtop に追加する変更は、コンパイラの最適化フェーズ (reorder1) が、インターフェース変換の後にこれらの代入を再順序付けしないようにするためのものです。これにより、可変引数の値が正しく構造体に格納され、その後のインターフェース変換が正しい値に基づいて行われることが保証されます。

fmt パッケージ側の変更

  • src/lib/fmt/print.go 内の全ての fmt 関数における reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue) から reflect.NewValue(a).(reflect.StructValue) への変更は、可変引数 a がもはやポインタとしてではなく、直接構造体としてリフレクションされるようになったことを明確に示しています。これは、コンパイラ側での TDDD 型の内部表現の変更と完全に同期しています。この変更により、fmt パッケージは可変引数の値をより直接的かつ効率的に処理できるようになり、余分なポインタのデリファレンスが不要になります。

これらの変更は、Go言語の可変引数メカニズムの内部実装をより堅牢で予測可能なものにし、特に defer 文との相互作用における潜在的なバグを修正することを目的としています。

関連リンク

参考にした情報源リンク

  • Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Go言語の reflect パッケージの進化に関する議論(GoのメーリングリストやIssueトラッカーなど、当時の情報源)
    • 当時のGo言語の設計思想やリフレクションの利用に関する情報は、現在の公式ドキュメントよりも、当時のメーリングリストや設計ドキュメントに多く見られます。
    • 例: Go言語の初期の設計ドキュメントやメーリングリストのアーカイブを検索することで、reflect.PtrValue の利用や defer と可変引数の相互作用に関する議論が見つかる可能性があります。
      • "Go language variadic functions defer"
      • "Go reflect.PtrValue early design"
      • "Go compiler TDDD TINTER"
      • "Go language commit 743ac07cc3c7761ecd808208911b106551a7ba4f"
  • Go言語のコンパイラ内部に関する一般的な情報源(現在のバージョンに基づくものが多いが、基本的な概念は共通)
    • "Go compiler internals"
    • "Go AST"
    • "Go type system implementation"
  • Go言語の fmt パッケージの内部実装に関する情報源
    • "Go fmt package internals"
    • "Go variadic arguments implementation"

(注: 2009年当時のGo言語に関する詳細な技術文書や議論は、現在のWeb検索では見つけにくい場合があります。当時のメーリングリストのアーカイブやGoプロジェクトの初期の設計ドキュメントがより正確な情報源となります。)