[インデックス 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言語のリフレクションに関する一般的な情報源