[インデックス 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 のような書式設定関数には明示的に存在しませんでした。
このコミットの背景には、以下のニーズがあったと考えられます。
- 汎用的な値の出力: 開発者がデバッグやログ出力のために、特定の型に依存しない形で任意の変数の内容を簡単に確認したいという要望。
fmt.Printlnとの整合性:fmt.Printlnが提供する出力形式を、fmt.Printfでも利用できるようにすることで、書式設定関数の使い分けをより直感的にする。- 配列の扱い: 配列やそのポインタを直接、かつ分かりやすい形式で出力する機能の必要性。特に、
reflectパッケージを通じて動的に型情報を扱う際に、配列の要素を適切に列挙して表示するメカニズムが求められていました。
これらの背景から、%v という新しいフォーマット指定子が導入され、fmt パッケージの柔軟性と表現力が大幅に向上しました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。
1. fmt パッケージ
fmt パッケージは、Go言語における書式設定されたI/O(入出力)を実装するためのパッケージです。C言語の printf や scanf に似た機能を提供し、文字列、数値、構造体などの値を整形して出力したり、入力から値を読み取ったりすることができます。
fmt.Printf: 書式指定文字列と引数に基づいて整形された文字列を標準出力に出力します。fmt.Println: 引数をスペースで区切り、最後に改行を追加して標準出力に出力します。各引数のデフォルトの「println」形式を使用します。- フォーマット指定子:
%d(整数),%s(文字列),%f(浮動小数点数) など、出力する値の型や形式を指定するための記号です。
2. reflect パッケージ
reflect パッケージは、Goプログラムが実行時に自身の構造を検査(リフレクション)するための機能を提供します。これにより、プログラムは変数の型、値、メソッドなどを動的に調べたり、操作したりすることができます。
reflect.Value: Goの任意の値を表す型です。reflect.ValueOf(x)を使うことで、任意の変数xをreflect.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 内で配列ポインタの特殊な処理を効率的に行うことができます。
doprintf と doprint の変更
doprintf:%vフォーマット指定子を処理するための新しいcase 'v'が追加され、p.printField(field)を呼び出すようになりました。%T(型を出力する指定子) の場合、Formatインターフェースを実装しているオブジェクトであっても、そのFormatメソッドを呼び出さないように変更されました。これは、型を出力する際にオブジェクト自身がその型を記述するロジックを上書きしないようにするためです。
doprint:- 以前は
doprint内に直接記述されていた様々な型の出力ロジックが、新しく導入されたp.printField(field)の呼び出しに置き換えられました。これにより、コードの重複が解消され、doprintの役割がより明確になりました。
- 以前は
これらの変更により、fmt パッケージはより堅牢で、拡張性があり、Go言語の型システムとリフレクション機能を活用して、多様なデータ型を統一的かつ柔軟に表示できるようになりました。
コアとなるコードの変更箇所
変更は src/lib/fmt/print.go ファイルに集中しています。
-
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; } -
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; } -
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; } }
-
doprint内の変更:- 既存の型ごとの出力ロジックを
p.printField(field)の呼び出しに置き換え:
(削除されたコードブロックは、was_string := p.printField(field); prev_string = was_string;printFieldに移動した内容とほぼ同じです。)
- 既存の型ごとの出力ロジックを
コアとなるコードの解説
getArrayPtr
この関数は、reflect.Value がポインタであり、そのポインタが配列を指している場合に、その配列の reflect.ArrayValue を安全に取得するためのユーティリティです。
v.Kind() == reflect.PtrKindで、まずvがポインタ型であるかを確認します。v.(reflect.PtrValue).Sub()を使って、ポインタが指す先の値のreflect.Valueを取得します。- その値が
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戻り値: このメソッドは、出力された値が文字列であったかどうかを示すブール値を返します。これはdoprintでprev_stringの状態を更新するために使用されます。
doprintf と doprint の変更の意義
doprintf:%vの追加により、fmt.Printfがfmt.Printlnと同様の汎用的な値の出力機能を持つようになりました。また、%Tの場合のFormatインターフェースの呼び出し抑制は、型情報の表示とカスタム書式設定の役割を明確に分離し、予期せぬ動作を防ぎます。doprint:printFieldの導入により、doprintは各フィールドの具体的な出力ロジックから解放され、フィールド間のスペースや改行の管理といった、より高レベルな役割に集中できるようになりました。これにより、コードのモジュール化が進み、可読性と保守性が向上しました。
これらの変更は、Go言語の fmt パッケージが、リフレクションを効果的に利用して、多様なデータ型に対して柔軟かつ一貫性のある書式設定機能を提供するための重要な一歩となりました。
関連リンク
- Go言語
fmtパッケージのドキュメント (現在のバージョン): https://pkg.go.dev/fmt - Go言語
reflectパッケージのドキュメント (現在のバージョン): https://pkg.go.dev/reflect - Go言語の
Stringerインターフェースに関する記事: https://go.dev/blog/strings
参考にした情報源リンク
- Go言語の公式ドキュメント (
fmt,reflectパッケージ) - Go言語のソースコード (
src/fmt/print.goの歴史的な変更履歴) - Go言語のフォーマット指定子に関する一般的な情報源
- Go言語のリフレクションに関する一般的な情報源