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

[インデックス 1331] ファイルの概要

このコミットは、Go言語の標準ライブラリであるfmtパッケージにおける書式設定機能の改善とバグ修正に関するものです。具体的には、構造体のメンバーを出力する際にフィールド名を表示する機能の追加、カスタムフォーマッタが書式設定フラグにアクセスできるようにする機能の拡張、そして構造体メンバーの文字列が意図せず連結されてしまう問題の修正が含まれています。

コミット

commit bf67afc84e456e03e5ca14e2e4039497b36051ec
Author: Russ Cox <rsc@golang.org>
Date:   Thu Dec 11 16:53:33 2008 -0800

    print field names on struct members.
    also don't concatenate strings next
    to each other in the struct,
    like p.doprint does.
    
    expose additional print flags to formatters
    
    R=r
    DELTA=128  (111 added, 11 deleted, 6 changed)
    OCL=20991
    CL=21018

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/bf67afc84e456e03e5ca14e2e4039497b36051ec

元コミット内容

このコミットは、以下の主要な変更を含んでいます。

  1. 構造体メンバーのフィールド名表示: fmtパッケージで構造体を出力する際に、フィールド名も一緒に表示する機能が追加されました。これは特にデバッグ時や構造体の内容を詳細に確認したい場合に有用です。
  2. フォーマッタへの追加フラグ公開: カスタムフォーマッタが、書式設定文字列で指定されたフラグ(例: -, +, #, , 0)の状態を問い合わせるための新しいメソッドがfmt.Formatterインターフェースに追加されました。これにより、カスタムフォーマッタはより柔軟な出力制御が可能になります。
  3. 構造体内の文字列連結問題の修正: 構造体のメンバーをプリントする際に、隣接する文字列フィールドが意図せず連結されてしまうバグが修正されました。

変更の背景

Go言語のfmtパッケージは、C言語のprintfに似た書式設定機能を提供しますが、Goの型システムとリフレクションを活用してより強力な機能を実現しています。このコミットが行われた2008年12月は、Go言語がまだ初期開発段階にあった時期であり、fmtパッケージも継続的に機能改善とバグ修正が行われていました。

このコミットの背景には、以下のニーズがあったと考えられます。

  • デバッグの利便性向上: 構造体の内容を%v動詞で出力する際、単に値が並ぶだけではどの値がどのフィールドに対応するのか分かりにくい場合があります。フィールド名が表示されることで、構造体の内容をより直感的に理解できるようになります。これは、特に複雑な構造体やネストされた構造体を扱う際に重要です。
  • カスタムフォーマッタの表現力向上: fmt.Formatterインターフェースを実装することで、任意の型が独自の書式設定ロジックを提供できます。しかし、既存のインターフェースでは、書式設定文字列で指定されたフラグ(例: %-v, %+v)の状態を直接取得する方法がありませんでした。この制限により、カスタムフォーマッタがフラグに応じた異なる出力を行うことが困難でした。フラグへのアクセスを可能にすることで、カスタムフォーマッタの表現力と柔軟性が大幅に向上します。
  • 出力の正確性の確保: 構造体メンバーの出力時に文字列が意図せず連結されるバグは、出力の正確性を損なうものであり、修正が必要でした。これは、fmtパッケージが提供する出力が常に期待通りであることを保証するために不可欠です。

これらの改善は、Go言語のfmtパッケージをより堅牢で使いやすくするための重要なステップでした。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念に関する前提知識が必要です。

  1. fmtパッケージ:

    • Go言語の標準ライブラリで、書式設定されたI/O(入出力)を提供します。fmt.Printf, fmt.Sprintf, fmt.Printlnなどの関数が含まれます。
    • 書式設定動詞 (Verbs): %v (デフォルトの書式設定), %d (整数), %s (文字列) など、様々な型に対応する動詞があります。
    • 書式設定フラグ (Flags): 動詞の前に置かれる修飾子で、出力の挙動を変更します。例えば、+フラグは数値の符号を表示したり、構造体のフィールド名を表示したりするために使われます。-フラグは左寄せ、#フラグは代替書式(例: 0xプレフィックス)などに使われます。
    • 幅 (Width)精度 (Precision): %10sのように幅を指定したり、%.2fのように浮動小数点数の精度を指定したりできます。
  2. reflectパッケージ:

    • Go言語のランタイムリフレクション機能を提供します。これにより、プログラムは実行時に自身の構造を検査し、操作することができます。
    • reflect.Value: 任意のGoの値を表す型です。
    • reflect.Type: 任意のGoの型を表す型です。
    • reflect.StructKind: reflect.Typeが構造体であることを示す定数です。
    • reflect.StructValue: 構造体の値を表すreflect.Valueの具体的な型です。
    • reflect.StructType: 構造体の型情報を表すreflect.Typeの具体的な型です。
    • Type().Field(i): 構造体のi番目のフィールドの型情報を取得します。
    • Len(): スライス、配列、マップ、構造体などの長さ(要素数やフィールド数)を取得します。
    • Get(): インターフェースの値を取得します。
  3. インターフェース (interface):

    • Go言語における抽象型であり、メソッドのシグネチャの集合を定義します。
    • fmt.Formatterインターフェース: Format(f Formatter, c int)メソッドを持つインターフェースです。このインターフェースを実装する型は、fmtパッケージの書式設定関数によって独自の出力ロジックを提供できます。ffmt.Formatter自身であり、cは書式設定動詞の文字コードです。
  4. ioパッケージ:

    • 基本的なI/Oプリミティブを提供します。
    • io.WriteString(w Writer, s string): wに文字列sを書き込みます。

これらの概念を理解することで、コミットがfmtパッケージの内部でどのようにリフレクションとインターフェースを活用して書式設定機能を拡張・改善しているかを深く把握できます。

技術的詳細

このコミットは、fmtパッケージの内部実装にいくつかの重要な変更を加えています。

  1. fmt.Formatterインターフェースの拡張:

    • src/lib/fmt/print.goにおいて、Formatterインターフェースに新しいメソッドFlag(int) boolが追加されました。
    • このメソッドは、書式設定動詞に適用された特定のフラグ(例: '-', '+', '#', ' ', '0')が設定されているかどうかを問い合わせるために使用されます。
    • これにより、カスタムフォーマッタは、書式設定文字列で指定されたフラグに基づいて、異なる出力ロジックを適用できるようになります。
  2. P構造体とFmt構造体の状態管理の統合:

    • 以前は、P構造体(プリンタの状態を管理する内部構造体)がwid, wid_ok, prec, prec_okといった幅と精度の状態を直接保持していました。
    • このコミットでは、これらのフィールドがP構造体から削除され、P.Width()P.Precision()メソッドがp.fmt.wid, p.fmt.wid_present, p.fmt.prec, p.fmt.prec_presentを参照するように変更されました。
    • これは、書式設定の状態(フラグ、幅、精度など)をFmt構造体(書式設定コンテキストを管理する内部構造体)に一元化するリファクタリングです。これにより、コードの重複が減り、状態管理がより明確になります。
    • Fmt.clearflags()メソッドも更新され、widprecもクリアされるようになりました。
  3. P.Flag()メソッドの実装:

    • P構造体にFlag(b int) boolメソッドが追加され、Formatterインターフェースの新しいメソッドを実装しています。
    • このメソッドは、引数b(フラグ文字のASCII値)に基づいて、p.fmt内の対応するブールフラグ(minus, plus, sharp, space, zero)の値を返します。
  4. 構造体メンバーの出力ロジックの改善 (printField):

    • src/lib/fmt/print.goprintField関数(リフレクションを使用して値をプリントする関数)内のreflect.StructKindのケースが大幅に変更されました。
    • 以前はp.doprint(field, true, false)という呼び出しがありましたが、これは削除されました。
    • 新しい実装では、構造体の各フィールドをループで処理し、p.printField(getField(v, i))を個別に呼び出すようになりました。
    • 重要な変更点として、donames := p.fmt.plus;という行が追加され、+フラグが設定されている場合にのみフィールド名を出力するロジックが導入されました。
    • 各フィールドの間にスペース' 'を追加することで、以前の「文字列が意図せず連結される」バグが修正されました。これにより、{abc def 123}のように、各フィールドが適切に区切られて出力されるようになります。
  5. 書式設定文字列の解析 (doprintf):

    • src/lib/fmt/print.godoprintf関数(書式設定文字列を解析し、値を出力する関数)も変更されました。
    • 書式設定フラグと幅/精度を解析する部分で、p.fmt.clearflags()が呼び出され、p.fmt.wid, p.fmt.wid_present, p.fmt.prec, p.fmt.prec_presentが直接使用されるようになりました。これは、前述のPFmtの状態管理統合の一環です。
    • 以前のp.wid, p.wid_ok, p.prec, p.prec_okへの直接代入は削除されました。

これらの変更により、fmtパッケージはより強力で柔軟な書式設定機能を提供し、特に構造体のデバッグ出力とカスタムフォーマッタの利用において大きな改善がもたらされました。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  • src/lib/fmt/fmt_test.go: 新しい機能のテストケースが追加されています。
  • src/lib/fmt/format.go: Fmt構造体のclearflagsメソッドが更新されています。
  • src/lib/fmt/print.go: Formatterインターフェースの定義、P構造体の実装、およびprintFielddoprintf関数のロジックが変更されています。

src/lib/fmt/fmt_test.go

--- a/src/lib/fmt/fmt_test.go
+++ b/src/lib/fmt/fmt_test.go
@@ -6,6 +6,7 @@ package fmt
 
 import (
 	"fmt";
+	"io";
 	"syscall";
 	"testing";
 )
@@ -163,3 +164,77 @@ export func TestSprintf(t *testing.T) {
 	}
 }
 
+type FlagPrinter struct { }
+func (*FlagPrinter) Format(f fmt.Formatter, c int) {
+	s := "%";
+	for i := 0; i < 128; i++ {
+		if f.Flag(i) {
+			s += string(i);
+		}
+	}
+	if w, ok := f.Width(); ok {
+		s += fmt.sprintf("%d", w);
+	}
+	if p, ok := f.Precision(); ok {
+		s += fmt.sprintf(".%d", p);
+	}
+	s += string(c);
+	io.WriteString(f, "["+s+"]");
+}
+
+type FlagTest struct {
+	in string;
+	out string;
+}
+
+var flagtests = []FlagTest {
+	FlagTest{ "%a", "[%a]" },
+	FlagTest{ "%-a", "[%-a]" },
+	FlagTest{ "%+a", "[%+a]" },
+	FlagTest{ "%#a", "[%#a]" },
+	FlagTest{ "% a", "[% a]" },
+	FlagTest{ "%0a", "[%0a]" },
+	FlagTest{ "%1.2a", "[%1.2a]" },
+	FlagTest{ "%-1.2a", "[%-1.2a]" },
+	FlagTest{ "%+1.2a", "[%+1.2a]" },
+	FlagTest{ "%-+1.2a", "[%+-1.2a]" },
+	FlagTest{ "%-1.2abc", "[%-1.2a]bc" },
+	FlagTest{ "%-+1.2abc", "[%+-1.2a]bc" },
+}
+
+export func TestFlagParser(t *testing.T) {
+	var flagprinter FlagPrinter;
+	for i := 0; i < len(flagtests); i++ {
+		tt := flagtests[i];
+		s := fmt.sprintf(tt.in, &flagprinter);
+		if s != tt.out {
+			t.Errorf("sprintf(%q, &flagprinter) => %q, want %q", tt.in, s, tt.out);
+		}
+	}
+}
+
+export func TestStructPrinter(t *testing.T) {
+	var s struct {
+		a string;
+		b string;
+		c int;
+	};
+	s.a = "abc";
+	s.b = "def";
+	s.c = 123;
+	type Test struct {
+		fmt string;
+		out string;
+	}
+	var tests = []Test {
+		Test{ "%v", "{abc def 123}" },
+		Test{ "%+v", "{a=abc b=def c=123}" },
+	};
+	for i := 0; i < len(tests); i++ {
+		tt := tests[i];
+		out := fmt.sprintf(tt.fmt, s);
+		if out != tt.out {
+			t.Errorf("sprintf(%q, &s) = %q, want %q", tt.fmt, out, tt.out);
+		}
+	}
+}

src/lib/fmt/format.go

--- a/src/lib/fmt/format.go
+++ b/src/lib/fmt/format.go
@@ -50,7 +50,9 @@ export type Fmt struct {
 }
 
 func (f *Fmt) clearflags() {
+	f.wid = 0;
 	f.wid_present = false;
+	f.prec = 0;
 	f.prec_present = false;
 	f.minus = false;
 	f.plus = false;

src/lib/fmt/print.go

--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -23,6 +23,9 @@ export type Formatter interface {
 	Write(b *[]byte) (ret int, err *os.Error);
 	Width()	(wid int, ok bool);
 	Precision()	(prec int, ok bool);
+
+	// flags
+	Flag(int)	bool;
 }
 
 type Format interface {
@@ -40,10 +43,6 @@ type P struct {
 	n	int;
 	buf	*[]byte;
 	fmt	*Fmt;
-\twid	int;
-\twid_ok	bool;
-\tprec	int;
-\tprec_ok	bool;
 }
 
 func Printer() *P {
@@ -53,11 +52,27 @@ func Printer() *P {
 }
 
 func (p *P) Width() (wid int, ok bool) {
-\treturn p.wid, p.wid_ok
+	return p.fmt.wid, p.fmt.wid_present
 }
 
 func (p *P) Precision() (prec int, ok bool) {
-\treturn p.prec, p.prec_ok
+	return p.fmt.prec, p.fmt.prec_present
+}
+
+func (p *P) Flag(b int) bool {
+	switch b {
+	case '-':
+		return p.fmt.minus;
+	case '+':
+		return p.fmt.plus;
+	case '#':
+		return p.fmt.sharp;
+	case ' ':
+		return p.fmt.space;
+	case '0':
+		return p.fmt.zero;
+	}
+	return false
 }
 
 func (p *P) ensure(n int) {
@@ -369,7 +384,21 @@ func (p *P) printField(field reflect.Value) (was_string bool) {
 		}
 	case reflect.StructKind:
 		p.add('{');
-\t\tp.doprint(field, true, false);
+		v := field.(reflect.StructValue);
+		t := v.Type().(reflect.StructType);
+		donames := p.fmt.plus;	// first p.printField clears flag
+		for i := 0; i < v.Len();  i++ {
+			if i > 0 {
+				p.add(' ')
+			}
+			if donames {
+				if name, typ, tag, off := t.Field(i); name != "" {
+					p.addstr(name);
+					p.add('=');
+				}
+			}
+			p.printField(getField(v, i));
+		}
 		p.add('}');
 	case reflect.InterfaceKind:
 		inter := field.(reflect.InterfaceValue).Get();
@@ -398,7 +427,8 @@ func (p *P) doprintf(format string, v reflect.StructValue) {
 			continue;
 		}
 		i++;
-\t\t// flags
+\t\t// flags and widths
+\t\tp.fmt.clearflags();
 		F: for ; i < end; i++ {
 			switch format[i] {
 			case '#':
@@ -416,11 +446,10 @@ func (p *P) doprintf(format string, v reflect.StructValue) {
 			}
 		}
 		// do we have 20 (width)?
-\t\tp.wid, p.wid_ok, i = parsenum(format, i, end);\n-\t\tp.prec_ok = false;\n+\t\tp.fmt.wid, p.fmt.wid_present, i = parsenum(format, i, end);\
 		// do we have .20 (precision)?
 		if i < end && format[i] == '.' {
-\t\t\tp.prec, p.prec_ok, i = parsenum(format, i+1, end);\n+\t\t\tp.fmt.prec, p.fmt.prec_present, i = parsenum(format, i+1, end);\
 		}
 		c, w = sys.stringtorune(format, i);\n 		i += w;\n@@ -445,12 +474,6 @@ func (p *P) doprintf(format string, v reflect.StructValue) {
 			}
 		}
 		s := "";
-\t\tif p.wid_ok {\n-\t\t\tp.fmt.w(p.wid);\n-\t\t}\n-\t\tif p.prec_ok {\n-\t\t\tp.fmt.p(p.prec);\n-\t\t}\n \t\tswitch c {\n \t\t\t// bool\n \t\t\tcase 't':

コアとなるコードの解説

fmt_test.go の変更点

  • FlagPrinter型とTestFlagParser関数:

    • FlagPrinterfmt.Formatterインターフェースを実装する新しい型です。
    • そのFormatメソッドは、渡されたfmt.Formatter(この場合はP構造体)のFlag(), Width(), Precision()メソッドを呼び出し、どのフラグ、幅、精度が設定されているかを文字列として構築します。
    • TestFlagParserは、様々な書式設定文字列(例: "%a", "%+a", "%1.2a")に対してFlagPrinterを使用し、期待される出力が得られるかを確認します。これにより、Formatterインターフェースの新しいFlagメソッドが正しく機能すること、および幅と精度の解析が正しく行われることを検証します。
  • TestStructPrinter関数:

    • このテストは、構造体の書式設定に関する新しい動作を検証します。
    • 特に注目すべきは、Test{ "%+v", "{a=abc b=def c=123}" }というテストケースです。これは、%+vという書式設定動詞を使用した場合に、構造体のフィールド名(a=, b=, c=)が値の前に表示されることを期待しています。これは、このコミットの主要な機能追加の一つです。
    • Test{ "%v", "{abc def 123}" }は、+フラグがない場合はフィールド名が表示されないことを確認します。

format.go の変更点

  • Fmt.clearflags()メソッド:
    • このメソッドは、書式設定フラグをリセットするために使用されます。
    • 変更前はf.wid_presentf.prec_presentのみをリセットしていましたが、このコミットでf.wid = 0;f.prec = 0;が追加され、幅と精度の値自体もリセットされるようになりました。これにより、書式設定の状態がより完全に初期化されるようになります。

print.go の変更点

  • Formatterインターフェース:

    • Flag(int) boolという新しいメソッドが追加されました。このメソッドは、カスタムフォーマッタが書式設定フラグの状態を問い合わせるためのAPIを提供します。
  • P構造体:

    • wid, wid_ok, prec, prec_okというフィールドが削除されました。これらの情報は、P構造体が持つfmt *Fmtフィールドを通じてFmt構造体から取得されるようになりました。これにより、書式設定の状態管理が一元化され、コードの重複が削減されます。
  • P.Width()およびP.Precision()メソッド:

    • これらのメソッドは、P構造体自身のフィールドではなく、p.fmtFmt構造体へのポインタ)のフィールドを参照するように変更されました。これにより、書式設定の状態がFmt構造体に集約されていることが明確になります。
  • P.Flag(b int) boolメソッド:

    • Formatterインターフェースの新しいFlagメソッドの実装です。
    • 引数b(フラグ文字のASCII値)に応じて、p.fmt内の対応するブールフラグ(minus, plus, sharp, space, zero)の値を返します。これにより、カスタムフォーマッタは、書式設定時にどのフラグが指定されたかをプログラム的に判断できます。
  • P.printField(field reflect.Value)メソッド:

    • reflect.StructKind(構造体)を処理する部分が大きく変更されました。
    • 以前はp.doprint(field, true, false)という汎用的なプリント関数を呼び出していましたが、これは削除されました。
    • 新しいロジックでは、構造体の各フィールドをループで個別に処理します。
    • donames := p.fmt.plus;という行が追加され、+フラグが設定されている場合にのみフィールド名を出力する条件が導入されました。
    • if donames { ... p.addstr(name); p.add('='); }の部分で、フィールド名と=記号が追加されます。
    • if i > 0 { p.add(' ') }という行が追加され、各フィールドの間にスペースが挿入されるようになりました。これにより、以前の「文字列が意図せず連結される」バグが修正され、構造体メンバーが適切に区切られて出力されるようになります。
  • P.doprintf(format string, v reflect.StructValue)メソッド:

    • 書式設定文字列の解析部分が変更されました。
    • フラグと幅/精度を解析する前にp.fmt.clearflags()が呼び出されるようになりました。これにより、各書式設定動詞の処理前にフラグの状態が確実にリセットされます。
    • 幅と精度の解析結果をp.wid, p.wid_ok, p.prec, p.prec_okに直接代入する代わりに、p.fmt.wid, p.fmt.wid_present, p.fmt.prec, p.fmt.prec_presentに直接代入するように変更されました。これは、Fmt構造体への状態の一元化を反映しています。
    • 以前存在した、p.wid_okp.prec_okに基づいてp.fmt.w()p.fmt.p()を呼び出すロジックが削除されました。これは、幅と精度の設定がFmt構造体内で直接行われるようになったため、不要になったためです。

これらの変更は、Go言語のfmtパッケージが、より洗練された書式設定機能を提供し、内部的な状態管理を改善するための重要なステップであったことを示しています。

関連リンク

参考にした情報源リンク