[インデックス 13295] ファイルの概要
このコミットは、Go言語の標準ライブラリであるfmt
パッケージにおける%#v
フォーマット動詞の意図しないセマンティクス変更を修正するものです。以前のコミット(CL 6245068)で行われた最適化(処理の順序変更による高速化)が原因で、%#v
の動作が変更されてしまっていました。このコミットでは、以前のセマンティクスを復元し、その動作を検証するためのテストケースを追加しています。
コミット
commit ee3c272611ab59ee68399596e5fb764b81a9dd8d
Author: Russ Cox <rsc@golang.org>
Date: Wed Jun 6 15:08:00 2012 -0400
fmt: fix inadvertent change to %#v
The reordering speedup in CL 6245068 changed the semantics
of %#v by delaying the clearing of some flags. Restore the old
semantics and add a test.
Fixes #3706.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6302048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ee3c272611ab59ee683995596e5fb764b81a9dd8d
元コミット内容
fmt: fix inadvertent change to %#v
The reordering speedup in CL 6245068 changed the semantics
of %#v by delaying the clearing of some flags. Restore the old
semantics and add a test.
Fixes #3706.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6302048
変更の背景
この変更は、Go言語のfmt
パッケージにおける%#v
フォーマット動詞の挙動が、以前のコミット(CL 6245068)によって意図せず変更されてしまった問題に対応するためのものです。CL 6245068は、fmt
パッケージ内の処理順序を変更することでパフォーマンスを向上させることを目的とした最適化でした。しかし、この最適化が、特定の内部フラグのクリアを遅延させるという副作用をもたらし、結果として%#v
の出力形式に影響を与えてしまいました。
具体的には、%#v
はGoの構文で値を表現する際に使用されるフォーマット動詞であり、通常は値の型情報や構造体のフィールド名など、より詳細な情報を含んだ形で出力されます。この最適化によって、一部のフラグが適切にクリアされなくなったため、%#v
が期待されるGo構文形式で出力されなくなるケースが発生しました。
この問題はIssue #3706として報告され、このコミットはその問題を修正し、%#v
が常に正しいGo構文形式で値を表現するように、以前のセマンティクスを復元することを目的としています。
前提知識の解説
fmt
パッケージ
fmt
パッケージは、Go言語におけるフォーマットI/O(入力/出力)を実装するための標準パッケージです。C言語のprintf
やscanf
に似た機能を提供し、様々なデータ型を整形して文字列として出力したり、文字列からデータを解析したりすることができます。
fmt.Printf
とフォーマット動詞
fmt.Printf
関数は、指定されたフォーマット文字列に従って引数を整形し、標準出力に出力します。フォーマット文字列には、値をどのように表示するかを制御する「フォーマット動詞」が含まれます。
%v
と%#v
フォーマット動詞
%v
(Value): 引数のデフォルトのフォーマットで値を出力します。これは、数値、文字列、ブール値、構造体、スライス、マップなど、ほとんどの型に適用されます。構造体の場合、フィールド名なしでフィールド値が出力されます。%#v
(Go-syntax Value): 引数をGo言語の構文で表現した形で出力します。これは、デバッグや、Goのコードとして再利用可能な形式で値を出力したい場合に特に便利です。構造体の場合、フィールド名とフィールド値が両方出力され、型情報も含まれることがあります。例えば、struct { A int }{A: 1}
のような形式で出力されます。
Goの内部的なフォーマット処理とフラグ
fmt
パッケージの内部では、値を整形する際に様々な内部フラグが使用されます。これらのフラグは、出力の形式(例えば、Go構文を使用するかどうか、符号を表示するかどうかなど)を制御します。%#v
のような特定のフォーマット動詞は、これらのフラグを適切に設定・クリアすることで、期待される出力形式を実現します。
リフレクション (Reflection)
Go言語のリフレクションは、プログラムの実行時に型情報を検査したり、変数の値を変更したりする機能です。fmt
パッケージは、特に構造体やインターフェースなど、コンパイル時に型が完全にわからない値を整形する際に、リフレクションを内部的に利用します。リフレクションは強力ですが、パフォーマンス上のオーバーヘッドがあるため、可能な限り避けるか、慎重に使用する必要があります。
技術的詳細
このコミットの技術的な核心は、fmt
パッケージの内部でフォーマット処理を行うpp
構造体のprintField
メソッドにおけるフラグの管理にあります。
以前のCL 6245068では、パフォーマンス向上のためにprintField
メソッド内の処理順序が変更されました。この変更により、p.fmt.plus
とp.fmt.sharp
という内部フラグのクリアが遅延されるようになりました。
p.fmt.plus
: 通常、数値の符号(+)を表示するかどうかを制御するフラグですが、%#v
の文脈ではGo構文の出力に影響を与える可能性があります。p.fmt.sharp
:%#v
の#
に対応し、Go構文形式での出力を指示するフラグです。
問題は、printField
がhandleMethods
というメソッドを呼び出す前にこれらのフラグがクリアされるべきであったにもかかわらず、最適化によってそのクリアが遅延されたことにありました。handleMethods
は、値がFormatter
インターフェースなどを実装している場合に、そのカスタムフォーマットロジックを呼び出す役割を担っています。もしフラグが適切にクリアされないままhandleMethods
が呼び出されると、カスタムフォーマットロジックが意図しないフラグの状態を見てしまい、結果として%#v
の出力が崩れる可能性がありました。
このコミットでは、この問題を解決するために、printField
メソッドの冒頭でoldPlus
とoldSharp
という変数に現在のフラグの状態を一時的に保存し、plus
とgoSyntax
(%#v
の場合にgoSyntax
がtrueになる)に基づいてp.fmt.plus
とp.fmt.sharp
をfalse
にクリアしています。そして、handleMethods
が呼び出される直前に、保存しておいたoldPlus
とoldSharp
の値を使ってフラグを元の状態に復元しています。
これにより、handleMethods
が呼び出される際には、フラグが常に期待されるクリーンな状態になっていることが保証され、%#v
のセマンティクスが正しく復元されます。コメントにもあるように、handleMethods
はコストの高い処理であるため、その呼び出しを遅延させることはパフォーマンス上有効ですが、フラグの管理をより慎重に行う必要があったということです。
コアとなるコードの変更箇所
このコミットによる変更は主に2つのファイルにわたります。
-
src/pkg/fmt/fmt_test.go
:fmttests
というテストケースのスライスに新しいエントリが追加されました。{"%#v", "foo",
"foo"}
というテストケースが追加されています。これは、文字列"foo"
を%#v
でフォーマットした場合に、期待される出力が"foo"
(引用符で囲まれた文字列リテラル)であることを検証します。これは、%#v
がGo構文で文字列を正しく表現していることを確認するためのものです。
-
src/pkg/fmt/print.go
:pp
構造体のprintField
メソッド内に、フラグのクリアと復元に関するロジックが追加されました。- 具体的には、
// Clear flags for base formatters.
というコメントの下に、oldPlus
とoldSharp
に現在のフラグの状態を保存し、plus
とgoSyntax
の条件に基づいてp.fmt.plus
とp.fmt.sharp
をfalse
に設定するコードが追加されています。 - そして、
// Restore flags in case handleMethods finds a Formatter.
というコメントの下に、保存しておいたoldPlus
とoldSharp
の値を使ってフラグを元の状態に戻すコードが追加されています。
コアとなるコードの解説
src/pkg/fmt/fmt_test.go
の変更
{"%#v", "foo", `\"foo\"`},
この行は、fmt
パッケージのテストスイートに新しいテストケースを追加しています。
- 最初の要素
"%#v"
は、使用するフォーマット動詞が%#v
であることを示します。 - 2番目の要素
"foo"
は、フォーマットされる入力値が文字列"foo"
であることを示します。 - 3番目の要素
"foo"
は、期待される出力結果が文字列"foo"
をGoの文字列リテラルとして表現したものであることを示します。つまり、二重引用符で囲まれた"foo"
です。
このテストケースの目的は、%#v
が文字列をGo構文で正しく表現できることを確認することです。以前のバグでは、この種の単純な値に対しても%#v
が期待通りに動作しない可能性があったため、このテストは修正が正しく機能していることを検証する上で重要です。
src/pkg/fmt/print.go
の変更
// Clear flags for base formatters.
// handleMethods needs them, so we must restore them later.
// We could call handleMethods here and avoid this work, but
// handleMethods is expensive enough to be worth delaying.
oldPlus := p.fmt.plus
oldSharp := p.fmt.sharp
if plus {
p.fmt.plus = false
}
if goSyntax {
p.fmt.sharp = false
}
このブロックは、printField
メソッドの初期段階で実行されます。
oldPlus := p.fmt.plus
とoldSharp := p.fmt.sharp
は、現在のp.fmt.plus
とp.fmt.sharp
フラグの状態を一時変数に保存しています。これは、後で元の状態に戻すために必要です。if plus { p.fmt.plus = false }
は、plus
フラグが設定されている場合(例えば%+v
のようなフォーマットの場合)に、p.fmt.plus
をfalse
にクリアします。if goSyntax { p.fmt.sharp = false }
は、goSyntax
フラグが設定されている場合(例えば%#v
のようなGo構文フォーマットの場合)に、p.fmt.sharp
をfalse
にクリアします。
この「クリア」の目的は、後続の基本的なフォーマッタ(bool
, string
, int
などのプリミティブ型を処理する部分)が、これらのフラグの影響を受けずにデフォルトの動作をするようにするためです。特に%#v
の場合、goSyntax
がtrueになり、p.fmt.sharp
がクリアされます。これは、プリミティブ型に対しては#
フラグが特別な意味を持たないため、余計な影響を与えないようにするためと考えられます。
default:
// Restore flags in case handleMethods finds a Formatter.
p.fmt.plus = oldPlus
p.fmt.sharp = oldSharp
// If the type is not simple, it might have methods.
if wasString, handled := p.handleMethods(verb, plus, goSyntax, depth); handled {
return wasString
}
このブロックは、printField
メソッドの後半、プリミティブ型で処理できなかった場合に実行されます。
p.fmt.plus = oldPlus
とp.fmt.sharp = oldSharp
は、以前に保存しておいたoldPlus
とoldSharp
の値を使って、フラグを元の状態に復元しています。- この復元は、その直後に呼び出される
p.handleMethods
のためです。handleMethods
は、フォーマットされる値がFormatter
インターフェースなどを実装している場合に、そのカスタムフォーマットロジックを呼び出します。カスタムフォーマットロジックは、fmt
パッケージの内部フラグの状態に依存する可能性があるため、handleMethods
が呼び出される前にフラグが元の(呼び出し元が期待する)状態に戻っていることが重要です。
この一連のフラグの保存、クリア、復元のロジックにより、fmt
パッケージは、以前の最適化によって生じた%#v
のセマンティクス変更を修正し、Go構文での値の表現が常に正しく行われるようにしています。
関連リンク
- Go言語の変更リスト (CL): https://golang.org/cl/6302048
- 関連するGo Issue: #3706 (コミットメッセージに記載されていますが、具体的なIssueの内容はコミットメッセージから推測する必要があります)
参考にした情報源リンク
- GitHubコミットページ: https://github.com/golang/go/commit/ee3c272611ab59ee68399596e5fb764b81a9dd8d
- Go言語
fmt
パッケージのドキュメント (Go公式ドキュメント): https://pkg.go.dev/fmt (一般的なfmt
パッケージの理解のため) - Go言語のリフレクションに関するドキュメント (Go公式ドキュメント): https://pkg.go.dev/reflect (リフレクションの概念理解のため)
- Go言語のソースコード (特に
src/pkg/fmt/print.go
): https://github.com/golang/go/blob/master/src/fmt/print.go (コードの変更内容の確認のため)