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

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

このコミットは、Go言語のfmtパッケージにおける書式設定のバグ修正に関するものです。具体的には、構造体(struct)を%+v書式指定子で出力する際に、+フラグが構造体の最初のフィールドに誤って引き継がれてしまう問題を解決します。これにより、期待通りの出力が得られないという不具合が修正されました。

コミット

commit 67a7abad7f1cfabc5715f6e47887d82186eb8d59
Author: Russ Cox <rsc@golang.org>
Date:   Tue Dec 16 14:39:29 2008 -0800

    clear flags so that %+v does not pass the +
    to the first field it prints.
    
    R=r
    DELTA=2  (1 added, 0 deleted, 1 changed)
    OCL=21324
    CL=21328
---
 src/lib/fmt/print.go | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/lib/fmt/print.go b/src/lib/fmt/print.go
index 9ac241f8dc..426bca1861 100644
--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -386,7 +386,8 @@ func (p *P) printField(field reflect.Value) (was_string bool) {
 	\tp.add('{');
 	\tv := field.(reflect.StructValue);
 	\tt := v.Type().(reflect.StructType);
-\t\tdonames := p.fmt.plus;\t// first p.printField clears flag
+\t\tdonames := p.fmt.plus;
+\t\tp.fmt.clearflags();\t// clear flags for p.printField
 	\tfor i := 0; i < v.Len();  i++ {
 	\t\tif i > 0 {
 	\t\t\tp.add(' ')

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

https://github.com/golang/go/commit/67a7abad7f1cfabc5715f6e47887d82186eb8d59

元コミット内容

%+vが最初のフィールドに+フラグを渡さないように、フラグをクリアする。

変更の背景

Go言語のfmtパッケージは、様々なデータ型を整形して出力するための機能を提供します。その中でも%vは任意の値をデフォルトの書式で出力する汎用的な書式指定子ですが、%+vは構造体(struct)の場合にフィールド名も出力するという特別な意味を持ちます。

このコミットが修正する問題は、%+vを使って構造体を出力する際に発生していました。fmtパッケージの内部では、書式指定子に付与されたフラグ(この場合は+)が内部的な状態として保持されます。構造体の各フィールドを再帰的に処理して出力する際、この+フラグが構造体全体の出力に適用されるだけでなく、誤って構造体の最初のフィールドの出力にも引き継がれてしまっていたのです。

例えば、%+vで構造体を出力しようとした場合、構造体自体の出力にはフィールド名が含まれることが期待されますが、その構造体の最初のフィールドがさらに別の構造体や特定の型であった場合、その最初のフィールドの出力にも意図せず+フラグが適用されてしまい、予期しない書式で出力される可能性がありました。これは、fmtパッケージの設計意図に反する動作であり、出力の整合性を損なうバグでした。

このバグを修正するために、構造体のフィールドを処理する前に、グローバルな書式フラグをクリアする必要がありました。

前提知識の解説

Go言語のfmtパッケージ

fmtパッケージは、Go言語における基本的な入出力フォーマット機能を提供します。fmt.Printffmt.Sprintfなどの関数を通じて、様々なデータ型を文字列に変換して出力することができます。

  • 書式指定子: %vは、値のデフォルトの書式表現を出力します。
  • フラグ: 書式指定子には、出力の挙動を制御するためのフラグを付与できます。
    • +フラグ: fmtパッケージの文脈では、%+vのように使用されると、構造体の場合にフィールド名を出力したり、数値の場合に常に符号を出力したりするなど、型によって異なる「詳細な」表現を提供します。

Go言語のreflectパッケージ

reflectパッケージは、実行時にプログラムの構造(型、フィールド、メソッドなど)を検査・操作するための機能を提供します。Go言語は静的型付け言語ですが、reflectパッケージを使用することで、動的な型情報の取得や、インターフェース値の基になる具体的な値の操作が可能になります。

  • reflect.Value: 任意のGoの値を表す型です。
  • reflect.StructValue: reflect.Valueの一種で、構造体の値を表します。
  • reflect.Type: Goの型情報を表すインターフェースです。
  • reflect.StructType: reflect.Typeの一種で、構造体の型情報を表します。

fmtパッケージは、構造体などの複雑なデータ型を整形して出力する際に、内部的にreflectパッケージを利用してその構造を解析し、各フィールドにアクセスします。

print.go内の関連構造体と関数

  • P構造体: fmtパッケージの内部で書式設定処理の状態を管理するための構造体です。出力バッファや現在の書式フラグなどの情報を含んでいます。

  • fmt構造体(p.fmt: P構造体の中にネストされた構造体で、現在の書式指定子に適用されているフラグ(例: pluswidthprecなど)を保持します。

    • plusフィールド: +フラグが設定されているかどうかを示すブール値です。
    • clearflags()メソッド: fmt構造体の全てのフラグをリセットするメソッドです。
  • printField(field reflect.Value)関数: P構造体のメソッドで、構造体の個々のフィールドを整形して出力する役割を担います。この関数は再帰的に呼び出される可能性があります。

技術的詳細

fmtパッケージの書式設定ロジックは、P構造体とそれに付随するfmt構造体によって管理されます。fmt構造体には、現在の書式指定子に適用されているフラグ(+, -, #, 0, など)がブール値や整数値として格納されています。

%+vで構造体を出力する場合、P.printField関数が呼び出され、構造体の各フィールドが順番に処理されます。この処理の際、p.fmt.plusというフラグがtrueに設定されていると、フィールド名が出力されるなどの特別な書式が適用されます。

問題は、p.fmt.plusフラグが構造体全体の出力のために設定された後、構造体の最初のフィールドを処理する際に、このフラグがクリアされずにそのまま引き継がれてしまっていた点にありました。これにより、最初のフィールドが例えば数値型であった場合、意図せず+フラグ(常に符号を出力する)が適用されてしまい、+123のように出力される可能性がありました。これは、%+vが構造体全体に適用されるべきであり、個々のフィールドの書式には影響を与えるべきではないという設計思想に反します。

このコミットの修正は、この「フラグの引き継ぎ」を防ぐことを目的としています。構造体のフィールドを処理する直前にp.fmt.clearflags()を呼び出すことで、構造体全体の+フラグはdonames変数に保存され、フィールドの出力には影響を与えないようにします。これにより、各フィールドは自身の型に応じたデフォルトの書式で出力され、%+vの意図通りの動作(構造体全体のフィールド名表示)が実現されます。

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

--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -386,7 +386,8 @@ func (p *P) printField(field reflect.Value) (was_string bool) {
 	\tp.add('{');
 	\tv := field.(reflect.StructValue);
 	\tt := v.Type().(reflect.StructType);
-\t\tdonames := p.fmt.plus;\t// first p.printField clears flag
+\t\tdonames := p.fmt.plus;
+\t\tp.fmt.clearflags();\t// clear flags for p.printField
 	\tfor i := 0; i < v.Len();  i++ {
 	\t\tif i > 0 {
 	\t\t\tp.add(' ')

コアとなるコードの解説

変更はsrc/lib/fmt/print.goファイルのP.printFieldメソッド内で行われています。

元のコードでは、以下の行がありました。

donames := p.fmt.plus;    // first p.printField clears flag

この行は、p.fmt.plus+フラグが設定されているか)の現在の状態をdonames変数に保存しています。コメントには「first p.printField clears flag」とありますが、実際にはこの行自体がフラグをクリアしているわけではありませんでした。このコメントは、おそらく将来の変更の意図を示していたか、あるいは誤解を招くものでした。

修正後のコードは以下のようになっています。

donames := p.fmt.plus;
p.fmt.clearflags();    // clear flags for p.printField
  1. donames := p.fmt.plus;: これは以前と同じで、+フラグが設定されているかどうかをdonames変数に保存します。このdonames変数は、構造体のフィールド名を出力するかどうかを制御するために使用されます。
  2. p.fmt.clearflags();: この行が今回の修正の核心です。 printFieldメソッドが構造体の個々のフィールドの処理を開始する直前に、p.fmt構造体内の全ての書式フラグ(pluswidthprecなど)をクリアします。

この変更により、%+vが構造体全体に適用された際に設定された+フラグが、構造体の最初のフィールドの出力に誤って引き継がれることがなくなります。各フィールドは、そのフィールド自身の型と値に基づいて、クリーンな状態で整形されるようになります。これにより、%+vの意図通りの動作、すなわち構造体全体のフィールド名表示は行われるが、個々のフィールドの書式には影響を与えないという挙動が保証されます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (src/lib/fmt/print.go)
  • Go言語のfmtパッケージとreflectパッケージに関する一般的なドキュメントと解説。
  • Go言語の書式設定に関する議論やフォーラム。