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

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

このコミットは、Go言語の標準ライブラリである fmt パッケージにおける書式設定機能の拡張に関するものです。具体的には、任意の値をその「println」形式で出力するための %v フォーマット指定子の追加と、配列(へのポインタを含む)を %v を通じて出力する機能が導入されました。これにより、fmt パッケージの汎用性と使いやすさが向上しています。

コミット

commit e2621b80374f74f07cd7e7c9265e2d20b242bdae
Author: Rob Pike <r@golang.org>
Date:   Thu Nov 13 15:20:52 2008 -0800

    add a %v format to print an arbitrary value in its "println" form.
    also add code to print (pointers to) arrays, through %v.
    
    R=rsc
    DELTA=108  (70 added, 33 deleted, 5 changed)
    OCL=19184
    CL=19192

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

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

元コミット内容

任意の値をその「println」形式で出力するための %v フォーマット指定子を追加し、また、配列(へのポインタを含む)を %v を通じて出力するコードを追加する。

変更の背景

Go言語の初期段階において、fmt パッケージは基本的な書式設定機能を提供していましたが、任意のデータ型を汎用的に、かつ人間が読みやすい形式で出力する統一されたメカニズムが不足していました。特に、fmt.Println のような関数が内部的に使用する「デフォルトの」または「自然な」表現で値を表示する手段が、fmt.Printf のような書式設定関数には明示的に存在しませんでした。

このコミットの背景には、以下のニーズがあったと考えられます。

  1. 汎用的な値の出力: 開発者がデバッグやログ出力のために、特定の型に依存しない形で任意の変数の内容を簡単に確認したいという要望。
  2. fmt.Println との整合性: fmt.Println が提供する出力形式を、fmt.Printf でも利用できるようにすることで、書式設定関数の使い分けをより直感的にする。
  3. 配列の扱い: 配列やそのポインタを直接、かつ分かりやすい形式で出力する機能の必要性。特に、reflect パッケージを通じて動的に型情報を扱う際に、配列の要素を適切に列挙して表示するメカニズムが求められていました。

これらの背景から、%v という新しいフォーマット指定子が導入され、fmt パッケージの柔軟性と表現力が大幅に向上しました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。

1. fmt パッケージ

fmt パッケージは、Go言語における書式設定されたI/O(入出力)を実装するためのパッケージです。C言語の printfscanf に似た機能を提供し、文字列、数値、構造体などの値を整形して出力したり、入力から値を読み取ったりすることができます。

  • fmt.Printf: 書式指定文字列と引数に基づいて整形された文字列を標準出力に出力します。
  • fmt.Println: 引数をスペースで区切り、最後に改行を追加して標準出力に出力します。各引数のデフォルトの「println」形式を使用します。
  • フォーマット指定子: %d (整数), %s (文字列), %f (浮動小数点数) など、出力する値の型や形式を指定するための記号です。

2. reflect パッケージ

reflect パッケージは、Goプログラムが実行時に自身の構造を検査(リフレクション)するための機能を提供します。これにより、プログラムは変数の型、値、メソッドなどを動的に調べたり、操作したりすることができます。

  • reflect.Value: Goの任意の値を表す型です。reflect.ValueOf(x) を使うことで、任意の変数 xreflect.Value 型に変換できます。
  • reflect.Kind: reflect.Value が表す値の基本的な種類(例: reflect.IntKind, reflect.StringKind, reflect.StructKind, reflect.PtrKind, reflect.ArrayKind など)を識別するための列挙型です。
  • reflect.Type: Goの任意の型を表す型です。reflect.TypeOf(x) を使うことで、任意の変数 x の型を reflect.Type 型に変換できます。
  • v.Kind(): reflect.Value の種類を返します。
  • v.Interface(): reflect.Value を元のGoのインターフェース値に変換します。
  • v.Field(i): 構造体の i 番目のフィールドの reflect.Value を返します。
  • v.Len(): 配列、スライス、マップ、文字列などの長さを返します。
  • v.Elem(i): 配列やスライスの i 番目の要素の reflect.Value を返します。ポインタの場合は、ポインタが指す先の値の reflect.Value を返します。
  • v.Sub(): ポインタが指す先の値の reflect.Value を返します。

3. インターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義します。型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを満たしていると見なされます。

  • String() メソッドを持つ型: String() string メソッドを持つ型は、fmt パッケージによって文字列として扱われる際に、このメソッドの戻り値が使用されます。
  • Format(f State, c rune) メソッドを持つ型: fmt.Formatter インターフェースを実装する型は、fmt.Printf のような関数でカスタムの書式設定を行うことができます。c はフォーマット指定子(例: 'v', 'd')を表します。

4. 「println」形式

fmt.Println 関数が値を表示する際のデフォルトの形式を指します。これは、通常、値の「自然な」または「人間が読みやすい」表現であり、文字列は引用符なしで、構造体はフィールド名と値のペアで、配列は要素をスペースで区切って表示されるなど、型に応じた適切な表現が選択されます。

技術的詳細

このコミットの主要な技術的変更点は、fmt パッケージが reflect パッケージをどのように利用して、任意の値を汎用的に書式設定するかという点にあります。

%v フォーマット指定子の導入

%v は "value" の略で、任意のGoの値をその「デフォルトの書式」で出力するために導入されました。これは fmt.Println が内部的に使用する形式と似ています。

  • 基本的な型の処理: 整数、浮動小数点数、ブール値、文字列などは、それぞれの標準的な文字列表現に変換されます。
  • String() メソッドを持つ型: 型が String() string メソッドを実装している場合、そのメソッドの戻り値が使用されます。これは、カスタムの文字列表現を提供するためのGoの慣習です。
  • 構造体: 構造体は {フィールド名: フィールド値 ...} の形式で出力されます。
  • ポインタ: ポインタは 0x プレフィックス付きの16進数アドレスとして出力されます。
  • 配列(とポインタ)の特殊処理: このコミットの重要な追加機能の一つは、配列とそのポインタの処理です。配列は &[要素1 要素2 ...] の形式で出力されます。これは、配列の要素を再帰的に %v 形式で出力することで実現されます。

printField メソッドの導入

以前は doprint 関数内に直接記述されていた様々な型の値の出力ロジックが、P (プリンタの状態を保持する構造体) の新しいメソッド printField に集約されました。これにより、コードの重複が削減され、保守性が向上しました。

printField メソッドは、reflect.Value を引数に取り、その Kind に応じて適切な書式設定を行います。

  • reflect.BoolKind, reflect.IntKind, reflect.UintKind, reflect.FloatKind, reflect.StringKind など、基本的な型はそれぞれの fmt 形式化関数 (p.fmt.boolean, p.fmt.d64, p.fmt.s など) を呼び出して文字列に変換します。
  • reflect.PtrKind の場合、ポインタが配列を指しているかどうかを getArrayPtr で確認し、もし配列であれば &[] で囲んで要素を再帰的に printField で出力します。そうでなければ、通常のポインタアドレスとして出力します。
  • reflect.StructKind の場合、{} で囲み、内部で doprint を再帰的に呼び出して構造体のフィールドを出力します。

getArrayPtr 関数の追加

この新しいヘルパー関数は、与えられた reflect.Value がポインタであり、かつそのポインタが配列を指している場合に、その配列の reflect.ArrayValue を返します。これにより、printField 内で配列ポインタの特殊な処理を効率的に行うことができます。

doprintfdoprint の変更

  • doprintf:
    • %v フォーマット指定子を処理するための新しい case 'v' が追加され、p.printField(field) を呼び出すようになりました。
    • %T (型を出力する指定子) の場合、Format インターフェースを実装しているオブジェクトであっても、その Format メソッドを呼び出さないように変更されました。これは、型を出力する際にオブジェクト自身がその型を記述するロジックを上書きしないようにするためです。
  • doprint:
    • 以前は doprint 内に直接記述されていた様々な型の出力ロジックが、新しく導入された p.printField(field) の呼び出しに置き換えられました。これにより、コードの重複が解消され、doprint の役割がより明確になりました。

これらの変更により、fmt パッケージはより堅牢で、拡張性があり、Go言語の型システムとリフレクション機能を活用して、多様なデータ型を統一的かつ柔軟に表示できるようになりました。

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

変更は src/lib/fmt/print.go ファイルに集中しています。

  1. getArrayPtr 関数の追加:

    func getArrayPtr(v reflect.Value) (val reflect.ArrayValue, ok bool) {
    	if v.Kind() == reflect.PtrKind {
    		v = v.(reflect.PtrValue).Sub();
    		if v.Kind() == reflect.ArrayKind {
    			return v.(reflect.ArrayValue), true;
    		}
    	}
    	return nil, false;
    }
    
  2. P.printField メソッドの追加:

    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
    	}
    	s := "";
    	switch field.Kind() {
    	case reflect.BoolKind:
    		s = p.fmt.boolean(field.(reflect.BoolValue).Get()).str();
    	case reflect.IntKind, reflect.Int8Kind, reflect.Int16Kind, reflect.Int32Kind, reflect.Int64Kind:
    		v, signed, ok := getInt(field);
    		s = p.fmt.d64(v).str();
    	case reflect.UintKind, reflect.Uint8Kind, reflect.Uint16Kind, reflect.Uint32Kind, reflect.Uint64Kind:
    		v, signed, ok := getInt(field);
    		s = p.fmt.ud64(uint64(v)).str();
    	case reflect.FloatKind, reflect.Float32Kind, reflect.Float64Kind, reflect.Float80Kind:
    		v, ok := getFloat(field);
    		s = p.fmt.g64(v).str();
    	case reflect.StringKind:
    		v, ok := getString(field);
    		s = p.fmt.s(v).str();
    		was_string = true;
    	case reflect.PtrKind:
    		// pointer to array?
    		if v, ok := getArrayPtr(field); ok {
    			p.addstr("&[");
    			for i := 0; i < v.Len(); i++ {
    				if i > 0 {
    					p.addstr(" ");
    				}
    				p.printField(v.Elem(i));
    			}
    			p.addstr("]");
    			break;
    		}
    		v, ok := getPtr(field);
    		p.add('0');
    		p.add('x');
    		s = p.fmt.uX64(v).str();
    	case reflect.StructKind:
    		p.add('{');
    		p.doprint(field, true, false);
    		p.add('}');
    	default:
    		s = "?" + field.Type().String() + "?";
    	}
    	p.addstr(s);
    	return was_string;
    }
    
  3. doprintf 内の変更:

    • %v のケースを追加:
      			// arbitrary value; do your best
      			case 'v':
      				p.printField(field);
      
    • %T の場合に Format インターフェースを呼び出さないように変更:
      		if c != 'T' {	// don't want thing to describe itself if we're asking for its type
      			if formatter, ok := field.Interface().(Format); ok {
      				formatter.Format(p, c);
      				continue;
      			}
      		}
      
  4. doprint 内の変更:

    • 既存の型ごとの出力ロジックを p.printField(field) の呼び出しに置き換え:
      		was_string := p.printField(field);
      		prev_string = was_string;
      
      (削除されたコードブロックは、printField に移動した内容とほぼ同じです。)

コアとなるコードの解説

getArrayPtr

この関数は、reflect.Value がポインタであり、そのポインタが配列を指している場合に、その配列の reflect.ArrayValue を安全に取得するためのユーティリティです。

  1. v.Kind() == reflect.PtrKind で、まず v がポインタ型であるかを確認します。
  2. v.(reflect.PtrValue).Sub() を使って、ポインタが指す先の値の reflect.Value を取得します。
  3. その値が reflect.ArrayKind であるかを確認し、もしそうであれば reflect.ArrayValue に型アサートして返します。 この関数により、printField 内で配列ポインタの特殊な処理を簡潔に記述できるようになりました。

P.printField

このメソッドは、fmt パッケージの書式設定ロジックの核心部分です。任意の reflect.Value を受け取り、その型に基づいて適切な文字列表現を生成し、プリンタのバッファに追加します。

  • String() インターフェースの優先: まず、値が String() メソッドを実装している String インターフェースを満たすかどうかをチェックします。もし満たしていれば、そのメソッドの戻り値が優先的に使用されます。これはGoの慣習であり、カスタムの文字列表現を提供するための標準的な方法です。
  • switch field.Kind(): 値の Kind に応じて異なる処理を行います。
    • プリミティブ型: BoolKind, IntKind, UintKind, FloatKind, StringKind などの基本的な型は、それぞれに対応する fmt 内部の書式設定関数(例: p.fmt.boolean, p.fmt.d64, p.fmt.s)を呼び出して、その型の標準的な文字列表現を取得します。
    • PtrKind (ポインタ):
      • getArrayPtr を呼び出して、ポインタが配列を指しているかどうかを確認します。
      • もし配列ポインタであれば、&[] で囲み、for ループで配列の各要素を v.Elem(i) で取得し、再帰的に p.printField(v.Elem(i)) を呼び出して出力します。これにより、ネストされた配列も適切に表示されます。要素間にはスペースが挿入されます。
      • 配列ポインタでなければ、通常のポインタアドレス(0x プレフィックス付きの16進数)として出力します。
    • StructKind (構造体): {} で囲み、p.doprint(field, true, false) を呼び出して構造体のフィールドを再帰的に出力します。doprint もまた、内部で printField を利用するため、構造体のフィールドも適切に書式設定されます。
    • その他: 未知の型やサポートされていない型の場合、"?Type?" の形式で出力されます。
  • was_string 戻り値: このメソッドは、出力された値が文字列であったかどうかを示すブール値を返します。これは doprintprev_string の状態を更新するために使用されます。

doprintfdoprint の変更の意義

  • doprintf: %v の追加により、fmt.Printffmt.Println と同様の汎用的な値の出力機能を持つようになりました。また、%T の場合の Format インターフェースの呼び出し抑制は、型情報の表示とカスタム書式設定の役割を明確に分離し、予期せぬ動作を防ぎます。
  • doprint: printField の導入により、doprint は各フィールドの具体的な出力ロジックから解放され、フィールド間のスペースや改行の管理といった、より高レベルな役割に集中できるようになりました。これにより、コードのモジュール化が進み、可読性と保守性が向上しました。

これらの変更は、Go言語の fmt パッケージが、リフレクションを効果的に利用して、多様なデータ型に対して柔軟かつ一貫性のある書式設定機能を提供するための重要な一歩となりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (fmt, reflect パッケージ)
  • Go言語のソースコード (src/fmt/print.go の歴史的な変更履歴)
  • Go言語のフォーマット指定子に関する一般的な情報源
  • Go言語のリフレクションに関する一般的な情報源