[インデックス 1044] ファイルの概要
このコミットは、Go言語のランタイムにおいて、インターフェースの型アサーション(interface{}.(Type)
)が成功したかどうかを示す ok
値を返す機能(いわゆる「comma-ok idiom」)をサポートするための基盤を導入するものです。具体的には、インターフェースから具体的な型への変換、およびインターフェースから別のインターフェースへの変換において、変換の成否を示すブーリアン値を返す新しいランタイム関数が追加されています。
コミット
commit e5d9a5c9f0861bc981c2e2677e35840650d262ff
Author: Russ Cox <rsc@golang.org>
Date: Mon Nov 3 17:34:37 2008 -0800
runtime support for interface ok,
whatever the final syntax ends up being.
R=ken
OCL=18414
CL=18414
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e5d9a5c9f0861bc981c2e2677e35840650d262ff
元コミット内容
このコミットは、Go言語のインターフェース型アサーションにおける「ok」値のランタイムサポートを追加します。最終的な構文がどうなるかにかかわらず、この機能の基盤を構築するものです。
変更の背景
Go言語の初期段階において、インターフェースの型アサーションは、変換が不可能な場合にパニック(panic)を引き起こす挙動でした。例えば、v := i.(T)
のようなコードで、インターフェース i
が型 T
の値を保持していない場合、プログラムは実行時エラーで終了してしまいます。これは、型アサーションの成否を安全にチェックし、それに基づいて異なる処理を行うようなユースケースに対応できませんでした。
このコミットは、このような問題を解決するために、型アサーションが成功したかどうかを示す bool
値を返すメカニズムをランタイムレベルで導入することを目的としています。これにより、開発者は v, ok := i.(T)
のような構文(いわゆる「comma-ok idiom」)を用いて、パニックを回避しつつ型アサーションの成否をプログラムで制御できるようになります。これはGo言語の堅牢性と柔軟性を高める上で非常に重要な変更でした。
前提知識の解説
Go言語のインターフェース
Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。具体的な実装を持たず、そのインターフェースで定義されたすべてのメソッドを実装する任意の型が、そのインターフェースを満たすと見なされます(構造的型付け)。これにより、異なる具体的な型を持つオブジェクトを統一的に扱うことが可能になります。
Goのインターフェース値は、内部的に2つの要素から構成されます。
- 型情報 (type): インターフェース値が保持している具体的な値の型(
_type
構造体へのポインタ)。 - 値情報 (value): インターフェース値が保持している具体的な値(データへのポインタ)。
型アサーション (Type Assertion)
型アサーションは、インターフェース値が特定の具体的な型を保持しているかどうかをチェックし、もし保持していればその具体的な型の値を取り出すためのGo言語の機能です。構文は value := interfaceValue.(Type)
のようになります。
初期のGoでは、このアサーションが失敗した場合(interfaceValue
が Type
の値を保持していない場合)、ランタイムパニックが発生しました。これは、エラーハンドリングの観点から望ましくない挙動であり、多くの言語で提供されているような安全な型キャストのメカニズムが求められていました。
Comma-ok Idiom
Go言語では、複数の戻り値を返す関数が一般的です。特に、操作の成功/失敗を示す bool
値を2番目の戻り値として返すパターンが頻繁に用いられます。これを「comma-ok idiom」と呼びます。例えば、マップからの値の取得 (value, ok := m[key]
) や、チャネルからの受信 (value, ok := <-ch
) などで利用されます。
このコミットは、型アサーションにもこの comma-ok
パターンを適用するためのランタイムサポートを導入するものです。これにより、value, ok := interfaceValue.(Type)
のように記述することで、ok
が true
ならアサーションが成功し value
に具体的な値が格納され、false
ならアサーションが失敗し value
はその型のゼロ値になる、という安全な挙動が実現されます。
Goコンパイラの構造 (gc, runtime)
src/cmd/gc
: Goコンパイラのフロントエンド部分です。Goのソースコードを解析し、中間表現に変換します。このコミットでは、コンパイラが新しいランタイム関数を認識し、それらを呼び出すための宣言が追加されています。src/runtime
: Goプログラムの実行をサポートするランタイムシステムです。ガベージコレクション、スケジューリング、プリミティブな型操作、インターフェースの内部処理などが含まれます。このコミットでは、インターフェースの型アサーションを実際に処理するC言語で書かれた関数が追加・修正されています。
技術的詳細
このコミットの核心は、インターフェースの型アサーションが失敗した場合にパニックを起こす代わりに、成功を示す bool
値を返す新しいランタイム関数を導入することです。
具体的には、以下の2つの新しいランタイム関数が追加されました。
-
sys·ifaceI2T2
: インターフェース型から具体的な型への変換(I.(T)
)を処理します。従来のsys·ifaceI2T
と異なり、変換された値と、変換が成功したかどうかを示すbool
値の2つを返します。im == nil
(インターフェースがnil) またはim->sigt != st
(インターフェースが保持する型が期待する型と異なる) の場合、ok
は0
(false) となり、ret
はゼロ値になります。- それ以外の場合、
ok
は1
(true) となり、ret
にインターフェースが保持する値が格納されます。
-
sys·ifaceI2I2
: インターフェース型から別のインターフェース型への変換(I.(J)
)を処理します。これも同様に、変換されたインターフェース値と、変換が成功したかどうかを示すbool
値の2つを返します。im == nil
(入力インターフェースがnil) の場合、出力インターフェースもnilとなり、ok
は1
(true) となります。これは、nilインターフェースからnilインターフェースへのアサーションは常に成功すると見なされるためです。im->sigi != si
(入力インターフェースの型情報が期待するインターフェース型と異なる) の場合、hashmap
を用いて新しいインターフェース型への変換を試みます。この変換が失敗した場合(例えば、入力インターフェースが期待するインターフェースのメソッドをすべて実装していない場合)、ok
は0
(false) となります。- それ以外の場合、
ok
は1
(true) となり、変換されたインターフェース値が返されます。
これらの関数は、Goコンパイラ (src/cmd/gc
) によって生成されるコードから呼び出されることになります。コンパイラは、v, ok := i.(T)
のような構文を検出すると、これらの新しいランタイム関数を呼び出すようにコードを生成します。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードスニペットは以下の通りです。
src/cmd/gc/sys.go
--- a/src/cmd/gc/sys.go
+++ b/src/cmd/gc/sys.go
@@ -30,7 +30,9 @@ export func arraystring(*[]byte) string;
export func ifaceT2I(sigi *byte, sigt *byte, elem any) (ret any);
export func ifaceI2T(sigt *byte, iface any) (ret any);
+export func ifaceI2T2(sigt *byte, iface any) (ret any, ok bool);
export func ifaceI2I(sigi *byte, iface any) (ret any);
+export func ifaceI2I2(sigi *byte, iface any) (ret any, ok bool);
export func ifaceeq(i1 any, i2 any) (ret bool);
export func reflect(i interface { }) (uint64, string);
export func unreflect(uint64, string) (ret interface { });
src/cmd/gc/sysimport.c
--- a/src/cmd/gc/sysimport.c
+++ b/src/cmd/gc/sysimport.c
@@ -22,7 +22,9 @@ char *sysimport =
"export func sys.arraystring (? *[]uint8) (? string)\\n"\n
"export func sys.ifaceT2I (sigi *uint8, sigt *uint8, elem any) (ret any)\\n"\n
"export func sys.ifaceI2T (sigt *uint8, iface any) (ret any)\\n"\n
+\t"export func sys.ifaceI2T2 (sigt *uint8, iface any) (ret any, ok bool)\\n"\n
"export func sys.ifaceI2I (sigi *uint8, iface any) (ret any)\\n"\n
+\t"export func sys.ifaceI2I2 (sigi *uint8, iface any) (ret any, ok bool)\\n"\n
"export func sys.ifaceeq (i1 any, i2 any) (ret bool)\\n"\n
"export func sys.reflect (i interface { }) (? uint64, ? string)\\n"\n
"export func sys.unreflect (? uint64, ? string) (ret interface { })\\n"\n
src/runtime/iface.c
--- a/src/runtime/iface.c
+++ b/src/runtime/iface.c
@@ -238,8 +238,6 @@ sys·ifaceT2I(Sigi *si, Sigt *st, void *elem, Map *retim, void *retit)\n void\n sys·ifaceI2T(Sigt *st, Map *im, void *it, void *ret)\n {\n-//\tint32 alg, wid;\n-\n \tif(debug) {\n \t\tprints(\"I2T sigt=\");\n \t\tprintsigt(st);\
@@ -250,22 +248,44 @@ sys·ifaceI2T(Sigt *st, Map *im, void *it, void *ret)\n \n \tif(im == nil)\n \t\tthrow(\"ifaceI2T: nil map\");\n-\n \tif(im->sigt != st)\n \t\tthrow(\"ifaceI2T: wrong type\");\n-\n-//\talg = st->hash;\n-//\twid = st->offset;\n-//\talgarray[alg].copy(wid, &ret, &it);\n \tret = it;\n-\n \tif(debug) {\n \t\tprints(\"I2T ret=\");\n \t\tsys·printpointer(ret);\n \t\tprints(\"\\n\");\n \t}\n+\tFLUSH(&ret);\n+}\n+\n+// ifaceI2T2(sigt *byte, iface any) (ret any, ok bool);\n+void\n+sys·ifaceI2T2(Sigt *st, Map *im, void *it, void *ret, bool ok)\n+{\n+\tif(debug) {\n+\t\tprints(\"I2T2 sigt=\");\n+\t\tprintsigt(st);\n+\t\tprints(\" iface=\");\n+\t\tprintiface(im, it);\n+\t\tprints(\"\\n\");\n+\t}\n \n+\tif(im == nil || im->sigt != st) {\n+\t\tret = 0;\n+\t\tok = 0;\n+\t} else {\n+\t\tret = it;\n+\t\tok = 1;\n+\t}\n+\tif(debug) {\n+\t\tprints(\"I2T2 ret=\");\n+\t\tsys·printpointer(ret);\n+\t\tsys·printbool(ok);\n+\t\tprints(\"\\n\");\n+\t}\n \tFLUSH(&ret);\n+\tFLUSH(&ok);\n }\n \n // ifaceI2I(sigi *byte, iface any) (ret any);\
@@ -302,6 +322,49 @@ sys·ifaceI2I(Sigi *si, Map *im, void *it, Map *retim, void *retit)\n \tFLUSH(&retit);\n }\n \n+// ifaceI2I2(sigi *byte, iface any) (ret any, ok bool);\n+void\n+sys·ifaceI2I2(Sigi *si, Map *im, void *it, Map *retim, void *retit, bool ok)\n+{\n+\tif(debug) {\n+\t\tprints(\"I2I2 sigi=\");\t\tprintsigi(si);\n+\t\tprints(\" iface=\");\n+\t\tprintiface(im, it);\n+\t\tprints(\"\\n\");\n+\t}\n+\n+\tif(im == nil) {\n+\t\t// If incoming interface is uninitialized (zeroed)\n+\t\t// make the outgoing interface zeroed as well.\n+\t\tretim = nil;\n+\t\tretit = nil;\n+\t\tok = 1;\n+\t} else {\n+\t\tretit = it;\n+\t\tretim = im;\n+\t\tok = 1;\n+\t\tif(im->sigi != si) {\n+\t\t\tretim = hashmap(si, im->sigt, 1);\n+\t\t\tif(retim == nil) {\n+\t\t\t\tretit = nil;\n+\t\t\t\tretim = nil;\n+\t\t\t\tok = 0;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tif(debug) {\n+\t\tprints(\"I2I ret=\");\n+\t\tprintiface(retim, retit);\n+\t\tprints(\"\\n\");\n+\t}\n+\n+\tFLUSH(&retim);\n+\tFLUSH(&retit);\n+\tFLUSH(&ok);\n+}\n+\n // ifaceeq(i1 any, i2 any) (ret bool);\n void\n sys·ifaceeq(Map *im1, void *it1, Map *im2, void *it2, byte ret)\n```
## コアとなるコードの解説
### `src/cmd/gc/sys.go` および `src/cmd/gc/sysimport.c` の変更
これらのファイルはGoコンパイラの一部であり、Goのランタイムが提供する「組み込み関数」や「システムコール」のようなものをコンパイラに認識させる役割を担っています。
* `sys.go`: Go言語で書かれたコンパイラのフロントエンドが、ランタイムのC言語関数を呼び出すためのGo言語側の宣言(`export func`)を追加しています。`ifaceI2T2` と `ifaceI2I2` が追加され、それぞれ `(ret any, ok bool)` という2つの戻り値を持つことが示されています。
* `sysimport.c`: コンパイラが内部的に使用するシステム関数のリストを定義するC言語の文字列です。ここにも新しい `ifaceI2T2` と `ifaceI2I2` のシグネチャが追加され、コンパイラがこれらの関数を正しくリンクできるようにしています。
これらの変更により、Goコンパイラは `v, ok := i.(T)` のような新しい構文を解析し、対応するランタイム関数 `sys·ifaceI2T2` または `sys·ifaceI2I2` を呼び出すコードを生成できるようになります。
### `src/runtime/iface.c` の変更
このファイルはGoランタイムのインターフェース処理の中核を担っています。追加された `sys·ifaceI2T2` と `sys·ifaceI2I2` 関数は、C言語で実装されており、インターフェースの内部構造(型情報 `Map *im` と値情報 `void *it`)を直接操作します。
* **`sys·ifaceI2T2` の実装**:
* この関数は、インターフェース `im, it` を具体的な型 `st` に変換しようとします。
* `im == nil` (インターフェースがnil) または `im->sigt != st` (インターフェースが保持する具体的な型が期待する型 `st` と一致しない) の場合、変換は失敗と判断されます。このとき、`ret` (戻り値) はゼロ値に設定され、`ok` は `0` (false) に設定されます。
* それ以外の場合、変換は成功と判断され、`ret` にインターフェースが保持する値 `it` がコピーされ、`ok` は `1` (true) に設定されます。
* `FLUSH(&ret)` と `FLUSH(&ok)` は、コンパイラの最適化によって変数の値がレジスタに保持されたままになることを防ぎ、メモリに書き戻すことを保証するためのマクロです。これは、GoのランタイムとC言語の間のインターフェースで特に重要です。
* **`sys·ifaceI2I2` の実装**:
* この関数は、インターフェース `im, it` を別のインターフェース型 `si` に変換しようとします。
* `im == nil` の場合、入力インターフェースがnilであるため、出力インターフェースもnilとなり、`ok` は `1` (true) となります。これは、nilインターフェースからnilインターフェースへのアサーションは常に成功と見なされるためです。
* `im->sigi != si` の場合、入力インターフェースの型情報 `im->sigi` が期待するインターフェース型 `si` と異なるため、`hashmap` 関数を使って新しいインターフェース型への変換(つまり、入力インターフェースが新しいインターフェースのメソッドセットを満たしているかどうかのチェック)を試みます。
* `hashmap` が `nil` を返した場合(変換が不可能、つまり入力インターフェースが期待するインターフェースのメソッドをすべて実装していない場合)、`retim` と `retit` は `nil` に設定され、`ok` は `0` (false) に設定されます。
* それ以外の場合、変換は成功し、`ok` は `1` (true) に設定されます。
* `FLUSH` マクロはここでも同様に、戻り値のメモリへの書き込みを保証します。
これらのランタイム関数の追加により、Go言語の型アサーションはより安全で柔軟なものとなり、Goの「comma-ok idiom」の重要な一部を形成することになりました。
## 関連リンク
* Go言語のインターフェースに関する公式ドキュメント: [https://go.dev/tour/methods/9](https://go.dev/tour/methods/9)
* Go言語の型アサーションに関する公式ドキュメント: [https://go.dev/tour/methods/15](https://go.dev/tour/methods/15)
* Go言語のcomma-ok idiomに関する解説(例: mapのアクセス): [https://go.dev/tour/moretypes/19](https://go.dev/tour/moretypes/19)
## 参考にした情報源リンク
* Go言語のソースコード (GitHub): [https://github.com/golang/go](https://github.com/golang/go)
* Go言語の初期の設計に関する議論(Go開発者ブログなど)
* Go言語のインターフェース実装に関する技術記事(例: "The Laws of Reflection" by Rob Pike)
* [https://go.dev/blog/laws-of-reflection](https://go.dev/blog/laws-of-reflection)
* Go言語のランタイムに関する技術記事やドキュメント