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

[インデックス 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言語のprintfscanfに似た機能を提供し、様々なデータ型を文字列に変換したり、文字列からデータを解析したりするために使用されます。

  • 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パッケージが様々な型の値を文字列に変換する際に、個々のフィールドや要素を処理するための内部ヘルパー関数です。

  1. 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;
    }
    
  2. 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

  1. 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;
    }
    
  2. 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.goTestArrayPrinter

このテスト関数は、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の変更点

  1. getArray関数: この関数は、reflect.Value型の引数vを受け取ります。 v.Kind()で値の基底型を調べ、それがreflect.ArrayKind(配列型)である場合にのみ、vreflect.ArrayValue型に型アサーションして返します。それ以外の場合は、nilfalseを返します。これは、printField関数内で配列型を安全に識別し、その要素にアクセスするためのゲートウェイとなります。

  2. printField関数内のcase reflect.ArrayKindブロック: printField関数は、reflect.Value型のfield引数を受け取り、その型に基づいて適切な出力ロジックを選択します。

    • case reflect.ArrayKind:: fieldが配列型である場合にこのブロックが実行されます。
    • if a, ok := getArray(field); ok: getArray関数を呼び出し、fieldが実際に配列であることを確認します。oktrueであれば、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言語の公式Gitリポジトリ: https://github.com/golang/go
  • Go言語の初期のコミット履歴(このコミットが含まれる)
  • Go言語のfmtおよびreflectパッケージのソースコード