[インデックス 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
パッケージのソースコード