[インデックス 1395] ファイルの概要
このコミットは、Go言語の標準ライブラリであるfmtパッケージにおいて、配列(array)が%vフォーマット指定子で正しく出力されない問題を修正するものです。具体的には、配列のポインタ(*array)は適切にフォーマットされていたものの、配列そのものが期待通りに表示されないという挙動を改善します。この変更により、fmt.Sprintf("%v", myArray)のように配列を直接渡した場合でも、その内容が[elem1 elem2 ...]のような形式で出力されるようになります。
コミット
commit b0d62676d2a01c96ab0bb9d328a85526498cd807
Author: Rob Pike <r@golang.org>
Date: Mon Dec 22 11:04:17 2008 -0800
print array (not just *array) using %v
TBR=rsc
DELTA=34 (33 added, 0 deleted, 1 changed)
OCL=21718
CL=21718
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b0d62676d2a01c96ab0bb9d328a85526498cd807
元コミット内容
print array (not just *array) using %v
TBR=rsc
DELTA=34 (33 added, 0 deleted, 1 changed)
OCL=21718
CL=21718
変更の背景
Go言語の初期開発段階において、fmtパッケージのフォーマット機能はまだ成熟していませんでした。特に、%v(デフォルトフォーマット)指定子を使用して配列を出力しようとした際に、配列そのものではなく、配列へのポインタ(*array)のみが正しくフォーマットされるという不整合がありました。これは、ユーザーが配列の内容を簡単にデバッグしたり、ログに出力したりする際に不便であり、直感的な挙動ではありませんでした。
このコミットは、この不整合を解消し、配列がポインタと同様に、その要素が適切に表示されるようにするために導入されました。これにより、fmtパッケージの使いやすさと一貫性が向上しました。
前提知識の解説
Go言語のfmtパッケージ
fmtパッケージは、Go言語におけるフォーマットされたI/O(入出力)を提供する標準ライブラリです。C言語のprintfやscanfに似た機能を提供し、様々なデータ型を文字列に変換したり、文字列からデータを解析したりするために使用されます。
fmt.Sprintf: 指定されたフォーマット文字列と引数に基づいて文字列を生成し、その結果の文字列を返します。- フォーマット指定子(Verb):
fmtパッケージでは、出力する値の型や表示形式を制御するために、%v、%d、%sなどのフォーマット指定子(verb)を使用します。%v: 値のデフォルトフォーマット。Goの構文で値を表示します。構造体や配列など、複合型の内容を再帰的に表示する際に便利です。%T: 値の型を表示します。
Go言語のreflectパッケージ
reflectパッケージは、Goプログラムの実行時に、変数や関数の型情報や値情報を動的に検査・操作するための機能を提供します。これは、ジェネリックな関数(例えば、任意の型の値を処理するfmtパッケージのようなもの)を実装する際に非常に重要です。
reflect.Value: Goの値を表す型です。reflect.ValueOf()関数を使って任意のGoの値からreflect.Valueを取得できます。reflect.Kind():reflect.Valueの基底型(プリミティブ型、構造体、配列、ポインタなど)を返します。例えば、配列であればreflect.ArrayKindを返します。reflect.ArrayKind:reflect.Kind列挙型の一つで、値が配列であることを示します。reflect.ArrayValue:reflectパッケージ内で配列の値を表すインターフェース(または構造体)です。これを通じて配列の長さ(Len())や個々の要素(Elem(i))にアクセスできます。
配列とスライス(Go言語の初期における文脈)
Go言語において、配列は固定長で、要素の型が同じである値のシーケンスです。スライスは可変長で、配列を基盤として構築されます。このコミットが作成された2008年時点では、Go言語はまだ初期段階であり、スライスと配列の扱いに関するセマンティクスやfmtパッケージでの表示方法も進化の途上にありました。このコミットは、特に配列の表示に関する初期の課題を解決するものです。
技術的詳細
このコミットの技術的な核心は、fmtパッケージが内部で利用しているreflectパッケージの機能を用いて、値が配列であるかどうかを正確に識別し、その要素を適切にフォーマットして出力するロジックを追加した点にあります。
変更は主にsrc/lib/fmt/print.goファイル内のprintField関数に集中しています。printField関数は、fmtパッケージが様々な型の値を文字列に変換する際に、個々のフィールドや要素を処理するための内部ヘルパー関数です。
-
getArray関数の追加: 以前は配列のポインタを扱うgetArrayPtr関数が存在しましたが、このコミットでは配列そのものを扱うための新しいヘルパー関数getArrayが追加されました。この関数はreflect.Valueを受け取り、そのKind()がreflect.ArrayKindである場合に、reflect.ArrayValueとして値を返します。これにより、printField内で配列型を明確に識別できるようになります。func getArray(v reflect.Value) (val reflect.ArrayValue, ok bool) { switch v.Kind() { case reflect.ArrayKind: return v.(reflect.ArrayValue), true; } return nil, false; } -
printField関数における配列の処理ロジックの追加:printField関数内のswitch v.Kind()文に、reflect.ArrayKindを処理するための新しいcaseブロックが追加されました。- このブロックでは、まず
getArray(field)を呼び出して、現在の値が配列であるかを確認し、もし配列であればreflect.ArrayValueを取得します。 - 次に、
a.Len()で配列の長さを取得し、ループを使って配列の各要素を反復処理します。 - 各要素は
a.Elem(i)で取得され、再帰的にp.printField()を呼び出すことでフォーマットされます。 - 要素間にはスペースが挿入され、配列全体は角括弧
[]で囲まれて出力されます。
これにより、
%vフォーマット指定子が配列に適用された際に、その内容が期待通りに表示されるようになりました。 - このブロックでは、まず
コアとなるコードの変更箇所
src/lib/fmt/fmt_test.go
新しいテストケースTestArrayPrinterが追加されました。このテストは、配列と配列のポインタの両方に対してfmt.sprintf("%v", ...)が期待通りの出力を生成するかどうかを検証します。
export func TestArrayPrinter(t *testing.T) {
a := []int{1, 2, 3, 4, 5};
want := "[1 2 3 4 5]";
out := fmt.sprintf("%v", a);
if out != want {
t.Errorf("sprintf(%%v, array) = %q, want %q", out, want);
}
want = "&" + want;
out = fmt.sprintf("%v", &a);
if out != want {
t.Errorf("sprintf(%%v, &array) = %q, want %q", out, want);
}
}
src/lib/fmt/print.go
-
getArray関数の追加:func getArray(v reflect.Value) (val reflect.ArrayValue, ok bool) { switch v.Kind() { case reflect.ArrayKind: return v.(reflect.ArrayValue), true; } return nil, false; } -
printField関数内のreflect.ArrayKindの処理追加:case reflect.ArrayKind: if a, ok := getArray(field); ok { p.addstr("["); for i := 0; i < a.Len(); i++ { if i > 0 { p.addstr(" "); } p.printField(a.Elem(i)); } p.addstr("]"); }
コアとなるコードの解説
src/lib/fmt/fmt_test.goのTestArrayPrinter
このテスト関数は、Goのテストフレームワーク(testingパッケージ)を使用して、fmt.Sprintfの挙動を検証します。
a := []int{1, 2, 3, 4, 5};: 整数のスライス(Goの初期では配列とスライスが密接に関連しており、この文脈では配列のテストとして機能します)を定義します。out := fmt.sprintf("%v", a);: スライスaを%vフォーマット指定子で文字列に変換します。if out != want { t.Errorf(...) }: 期待される出力"[1 2 3 4 5]"と比較し、一致しない場合はエラーを報告します。out = fmt.sprintf("%v", &a);: 次に、スライスaへのポインタ&aを%vフォーマット指定子で文字列に変換します。want = "&" + want;: ポインタの場合、期待される出力は&が前置された"&[1 2 3 4 5]"となります。- このテストは、配列そのものと配列のポインタの両方が、
%vフォーマットで正しく表示されることを保証するためのものです。
src/lib/fmt/print.goの変更点
-
getArray関数: この関数は、reflect.Value型の引数vを受け取ります。v.Kind()で値の基底型を調べ、それがreflect.ArrayKind(配列型)である場合にのみ、vをreflect.ArrayValue型に型アサーションして返します。それ以外の場合は、nilとfalseを返します。これは、printField関数内で配列型を安全に識別し、その要素にアクセスするためのゲートウェイとなります。 -
printField関数内のcase reflect.ArrayKindブロック:printField関数は、reflect.Value型のfield引数を受け取り、その型に基づいて適切な出力ロジックを選択します。case reflect.ArrayKind::fieldが配列型である場合にこのブロックが実行されます。if a, ok := getArray(field); ok:getArray関数を呼び出し、fieldが実際に配列であることを確認します。okがtrueであれば、aにはreflect.ArrayValueが格納されます。p.addstr("[");: 出力文字列に開始角括弧[を追加します。for i := 0; i < a.Len(); i++ { ... }:a.Len()で配列の要素数を取得し、各要素をループで処理します。if i > 0 { p.addstr(" "); }: 最初の要素以外の場合、要素間にスペースを追加して区切ります。p.printField(a.Elem(i));:a.Elem(i)で現在のインデックスiの要素のreflect.Valueを取得し、printField自身を再帰的に呼び出してその要素をフォーマットします。これにより、多次元配列や配列内の構造体なども適切に処理されます。p.addstr("]");: 出力文字列に終了角括弧]を追加します。
この変更により、fmtパッケージはreflectの機能を使って配列の内部構造を動的に解析し、その内容を人間が読みやすい形式で出力できるようになりました。
関連リンク
- Go言語の
fmtパッケージ公式ドキュメント: https://pkg.go.dev/fmt - Go言語の
reflectパッケージ公式ドキュメント: https://pkg.go.dev/reflect - Go言語の配列とスライスに関する公式ドキュメント(現在のバージョン): https://go.dev/blog/go-slices-usage-and-internals
参考にした情報源リンク
- Go言語の公式Gitリポジトリ: https://github.com/golang/go
- Go言語の初期のコミット履歴(このコミットが含まれる)
- Go言語の
fmtおよびreflectパッケージのソースコード