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

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

このコミットは、Go言語の初期段階(2008年12月)において、reflectパッケージとfmtパッケージにおけるnilインターフェースの取り扱いを改善することを目的としています。具体的には、nilインターフェースが正しく識別され、適切にフォーマットされるように、コードの修正が行われています。

コミット

  • コミットハッシュ: ac09eb4f49a409e4b99638cadac39bc13cf6816f
  • 作者: Rob Pike r@golang.org
  • 日付: 2008年12月11日 木曜日 12:59:49 -0800
  • コミットメッセージ:
    handle the nil interface better in reflect and print
    
    R=rsc
    DELTA=25  (19 added, 0 deleted, 6 changed)
    OCL=20985
    CL=20985
    

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

https://github.com/golang/go/commit/ac09eb4f49a409e4b99638cadac39bc13cf6816f

元コミット内容

reflectおよびprint(現在のfmtパッケージの前身)において、nilインターフェースの取り扱いを改善する。

変更の背景

Go言語におけるインターフェースは、型と値のペアとして内部的に表現されます。インターフェース変数がnilであると判断されるのは、その型と値の両方がnilである場合のみです。しかし、インターフェース変数の値がnilであっても、型がnilでない場合(例えば、具体的なnilポインタがインターフェースに代入された場合)は、そのインターフェース変数はnilとは見なされません。

このコミット以前のreflectおよびfmtパッケージでは、このような「型を持つnilインターフェース」の取り扱いが不十分であったと考えられます。特に、fmtパッケージでの値の出力時や、reflectパッケージでの型情報の解析時に、nilインターフェースが正しく識別されず、予期せぬ動作や誤った出力が発生する可能性がありました。

この変更は、nilインターフェースがfmtパッケージで適切に文字列化され、reflectパッケージでその型が正しく認識されるようにすることで、Goプログラムの堅牢性と予測可能性を高めることを目的としています。

前提知識の解説

Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。インターフェース型は、そのインターフェースが定義するすべてのメソッドを実装する任意の具象型の値を保持できます。

Goのインターフェースは内部的に2つの要素、すなわち「型(type)」と「値(value)」のペアとして表現されます。

  • 型 (Type): インターフェースが保持している具象値の型。
  • 値 (Value): インターフェースが保持している具象値そのもの。

インターフェース変数がnilであると評価されるのは、この「型」と「値」の両方がnilである場合のみです。もし、値がnilであっても、型がnilでない場合(例: var p *MyStruct = nil; var i interface{} = p; の場合、inilではない)、そのインターフェースはnilとは見なされません。これはGo言語のインターフェースの重要な特性であり、しばしば開発者を混乱させる点でもあります。

reflectパッケージ

reflectパッケージは、実行時にGoプログラムの構造を検査(リフレクション)するための機能を提供します。これにより、変数の型、値、メソッドなどを動的に調べたり、操作したりすることが可能になります。このパッケージは、汎用的なシリアライザ、デシリアライザ、RPCシステム、テストフレームワークなどの構築に不可欠です。

fmtパッケージ

fmtパッケージは、Go言語におけるフォーマットされたI/O(入出力)を実装します。これは、C言語のprintfscanfに似た機能を提供し、様々な型の値を文字列に変換して出力したり、文字列から値を解析したりするために使用されます。このコミットの時点では、src/lib/fmt/print.goというパスに存在しており、現在のfmtパッケージの原型となっています。

StringインターフェースとFormatインターフェース

Go言語のfmtパッケージは、カスタムの文字列フォーマットを可能にするために、特定のインターフェースを認識します。

  • Stringインターフェース: String() string メソッドを持つ型は、fmtパッケージによって文字列としてフォーマットされる際に、このメソッドが呼び出されます。
  • Formatインターフェース: Format(s fmt.State, verb rune) メソッドを持つ型は、より詳細なフォーマット制御(例: %v, %+v, %#vなどの動詞に応じた出力)を提供できます。

これらのインターフェースは、Goのポリモーフィズムの強力な例であり、fmtパッケージが様々なカスタム型を適切に表示できるようにします。

技術的詳細

このコミットは、主に以下の2つのシナリオにおけるnilインターフェースの取り扱いを改善しています。

  1. fmtパッケージにおけるString()およびFormat()メソッドの呼び出し: 以前の実装では、field.Interface().(String)field.Interface().(Format)のように、インターフェースの具象値を取得してから型アサーションを行っていました。しかし、field.Interface()が「型を持つnilインターフェース」を返した場合、その具象値はnilであっても、インターフェース自体はnilではないため、型アサーションが成功し、String()Format()メソッドがnilレシーバで呼び出されてしまう可能性がありました。これはランタイムパニックを引き起こす原因となります。 このコミットでは、inter := field.Interface(); if inter != nil { ... } という明示的なnilチェックを追加することで、具象値がnilである場合にはこれらのメソッドが呼び出されないように修正しています。

  2. fmtパッケージにおけるreflect.InterfaceKindの処理: fmtパッケージがreflect.InterfaceKindの値を処理する際に、インターフェースがnilであるかどうかを正確に判断し、適切な文字列(<nil>)を出力するように変更されています。以前は、nilインターフェースが正しく識別されず、誤った出力や未定義の動作を引き起こす可能性がありました。

  3. reflectパッケージにおけるnilインターフェースの型表現: reflectパッケージにNilInterfaceという特別なInterfaceTypeStructが導入されました。これは、型情報を持たない純粋なnilインターフェースの型を表します。また、ParseTypeString関数が空の文字列を受け取った場合に、このNilInterfaceを返すように修正されました。これにより、reflectパッケージがnilインターフェースの型をより正確に表現できるようになります。

  4. reflectパッケージにおけるMissingValueKind: MissingValueStructCommonフィールドのIntKindMissingKindに変更されました。これは、値が欠落している状態をより正確に表現するための変更です。

これらの変更により、Go言語のランタイムがnilインターフェースをより堅牢かつ予測可能に処理できるようになり、デバッグが容易になり、プログラムの安定性が向上しました。

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

このコミットでは、以下の3つのファイルが変更されています。

  1. src/lib/fmt/print.go:

    • printField関数内で、field.Interface()の結果をinter変数に格納し、inter != nilのチェックを追加してからString()インターフェースの型アサーションを行うように変更。
    • reflect.InterfaceKindの場合のswitch文に新しいcaseを追加し、field.(reflect.InterfaceValue).Get()nilであれば"<nil>"、そうでなければ"<non-nil interface>"を出力するように修正。
    • doprintf関数内で、field.Interface()の結果をinter変数に格納し、inter != nilのチェックを追加してからFormat()インターフェースの型アサーションを行うように変更。
  2. src/lib/reflect/type.go:

    • NilInterfaceというグローバル変数を追加。これは、型情報を持たないnilインターフェースの型を表すInterfaceTypeStructのインスタンス。
    • ParseTypeString関数内で、typestringが空文字列の場合にNilInterfaceを返すように修正。
  3. src/lib/reflect/value.go:

    • MissingValueインターフェースにAddr()メソッドを追加。
    • MissingCreator関数内で、MissingValueStructの初期化時にCommonフィールドのIntKindMissingKindに変更。

コアとなるコードの解説

src/lib/fmt/print.go

--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -307,9 +307,12 @@ func parsenum(s string, start, end int) (n int, got bool, newi int) {
 }
 
 func (p *P) printField(field reflect.Value) (was_string bool) {
-	if stringer, ok := field.Interface().(String); ok {
-		p.addstr(stringer.String());
-		return false;	// this value is not a string
+	inter := field.Interface();
+	if inter != nil {
+		if stringer, ok := inter.(String); ok {
+			p.addstr(stringer.String());
+			return false;	// this value is not a string
+		}
 	}
 	s := "";
 	switch field.Kind() {
@@ -363,6 +366,14 @@ func (p *P) printField(field reflect.Value) (was_string bool) {
 		p.add('{');
 		p.doprint(field, true, false);
 		p.add('}');
+	case reflect.InterfaceKind:
+		inter := field.(reflect.InterfaceValue).Get();
+		if inter == nil {
+			s = "<nil>"
+		} else {
+			// should never happen since a non-nil interface always has a type
+			s = "<non-nil interface>";
+		}
 	default:
 		s = "?" + field.Type().String() + "?";
 	}
@@ -421,8 +432,9 @@ func (p *P) doprintf(format string, v reflect.StructValue) {
 		}
 		field := getField(v, fieldnum);
 		fieldnum++;
-		if c != 'T' {	// don't want thing to describe itself if we're asking for its type
-			if formatter, ok := field.Interface().(Format); ok {
+		inter := field.Interface();
+		if inter != nil && c != 'T' {	// don't want thing to describe itself if we're asking for its type
+			if formatter, ok := inter.(Format); ok {
 				formatter.Format(p, c);
 				continue;
 			}
  • printField関数とdoprintf関数において、field.Interface()の結果を直接型アサーションするのではなく、一度inter変数に代入し、inter != nilという明示的なnilチェックを追加しています。これにより、具象値がnilであるインターフェース(型はnilではない)に対してString()Format()メソッドが呼び出されるのを防ぎ、ランタイムパニックを回避します。
  • printField関数のswitch field.Kind()において、reflect.InterfaceKindの新しいcaseが追加されました。ここでは、インターフェースの具象値がnilであるかをチェックし、nilであれば"<nil>"と出力します。elseブロックのコメント「should never happen since a non-nil interface always has a type」は、型を持つインターフェースがnilでないことを示唆しています。

src/lib/reflect/type.go

--- a/src/lib/reflect/type.go
+++ b/src/lib/reflect/type.go
@@ -340,6 +340,8 @@ func (t *InterfaceTypeStruct) Len() int {
 	return len(t.field)
 }
 
+var NilInterface = NewInterfaceTypeStruct("nil", "", new([]Field, 0));
+
 // -- Func
 
 export type FuncType interface {
@@ -834,6 +836,10 @@ func (p *Parser) Type(name string) *StubType {
 }
 
 export func ParseTypeString(name, typestring string) Type {
+	if typestring == "" {
+		// If the typestring is empty, it represents (the type of) a nil interface value
+		return NilInterface
+	}
 	p := new(Parser);
 	p.str = typestring;
 	p.Next();
  • NilInterfaceというvarが追加されました。これは、型名が"nil"で、フィールドを持たないInterfaceTypeStructとして初期化されます。これは、Goの型システムにおいて、純粋なnilインターフェース(型情報も値もnil)の型を表現するための特別な定数として機能します。
  • ParseTypeString関数にif typestring == ""の条件が追加されました。typestringが空の場合、それはnilインターフェースの型を表すと解釈され、新しく定義されたNilInterfaceが返されます。これにより、reflectパッケージがnilインターフェースの型をより正確に解析できるようになります。

src/lib/reflect/value.go

--- a/src/lib/reflect/value.go
+++ b/src/lib/reflect/value.go
@@ -59,6 +59,7 @@ type Creator *(typ Type, addr Addr) Value
 export type MissingValue interface {
 	Kind()	int;
 	Type()	Type;
+	Addr()	Addr;
 }
 
 type MissingValueStruct struct {
@@ -66,7 +67,7 @@ type MissingValueStruct struct {
 }
 
 func MissingCreator(typ Type, addr Addr) Value {
-	return &MissingValueStruct{ Common{IntKind, typ, addr} }
+	return &MissingValueStruct{ Common{MissingKind, typ, addr} }
 }
 
 // -- Int
  • MissingValueインターフェースにAddr()メソッドが追加されました。これは、MissingValueがアドレスを持つことができることを示唆しています。
  • MissingCreator関数において、MissingValueStructCommonフィールドのKindIntKindからMissingKindに変更されました。これは、この値が整数型ではなく、「欠落している」という特別な種類であることをより明確に示しています。

これらの変更は、Go言語の初期段階における型システムとリフレクションの基盤を強化し、nilインターフェースのセマンティクスをより正確に反映させるための重要なステップでした。

関連リンク

参考にした情報源リンク

  • Go言語のインターフェースの内部表現に関する議論(Stack Overflowなど、一般的なGoコミュニティの知識)
  • Go言語の初期のコミット履歴と設計思想に関する情報(Goの公式リポジトリのコミットログや関連するメーリングリストのアーカイブ)
  • Go言語のreflectパッケージの進化に関する記事やドキュメント
  • Go言語のfmtパッケージの歴史に関する情報