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

[インデックス 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言語のprintfscanfに似た機能を提供し、様々なデータ型を整形して文字列として出力したり、文字列からデータを解析したりすることができます。

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.plusp.fmt.sharpという内部フラグのクリアが遅延されるようになりました。

  • p.fmt.plus: 通常、数値の符号(+)を表示するかどうかを制御するフラグですが、%#vの文脈ではGo構文の出力に影響を与える可能性があります。
  • p.fmt.sharp: %#v#に対応し、Go構文形式での出力を指示するフラグです。

問題は、printFieldhandleMethodsというメソッドを呼び出す前にこれらのフラグがクリアされるべきであったにもかかわらず、最適化によってそのクリアが遅延されたことにありました。handleMethodsは、値がFormatterインターフェースなどを実装している場合に、そのカスタムフォーマットロジックを呼び出す役割を担っています。もしフラグが適切にクリアされないままhandleMethodsが呼び出されると、カスタムフォーマットロジックが意図しないフラグの状態を見てしまい、結果として%#vの出力が崩れる可能性がありました。

このコミットでは、この問題を解決するために、printFieldメソッドの冒頭でoldPlusoldSharpという変数に現在のフラグの状態を一時的に保存し、plusgoSyntax%#vの場合にgoSyntaxがtrueになる)に基づいてp.fmt.plusp.fmt.sharpfalseにクリアしています。そして、handleMethodsが呼び出される直前に、保存しておいたoldPlusoldSharpの値を使ってフラグを元の状態に復元しています。

これにより、handleMethodsが呼び出される際には、フラグが常に期待されるクリーンな状態になっていることが保証され、%#vのセマンティクスが正しく復元されます。コメントにもあるように、handleMethodsはコストの高い処理であるため、その呼び出しを遅延させることはパフォーマンス上有効ですが、フラグの管理をより慎重に行う必要があったということです。

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

このコミットによる変更は主に2つのファイルにわたります。

  1. src/pkg/fmt/fmt_test.go:

    • fmttestsというテストケースのスライスに新しいエントリが追加されました。
    • {"%#v", "foo", "foo"}というテストケースが追加されています。これは、文字列"foo"%#vでフォーマットした場合に、期待される出力が"foo"(引用符で囲まれた文字列リテラル)であることを検証します。これは、%#vがGo構文で文字列を正しく表現していることを確認するためのものです。
  2. src/pkg/fmt/print.go:

    • pp構造体のprintFieldメソッド内に、フラグのクリアと復元に関するロジックが追加されました。
    • 具体的には、// Clear flags for base formatters.というコメントの下に、oldPlusoldSharpに現在のフラグの状態を保存し、plusgoSyntaxの条件に基づいてp.fmt.plusp.fmt.sharpfalseに設定するコードが追加されています。
    • そして、// Restore flags in case handleMethods finds a Formatter.というコメントの下に、保存しておいたoldPlusoldSharpの値を使ってフラグを元の状態に戻すコードが追加されています。

コアとなるコードの解説

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.plusoldSharp := p.fmt.sharpは、現在のp.fmt.plusp.fmt.sharpフラグの状態を一時変数に保存しています。これは、後で元の状態に戻すために必要です。
  • if plus { p.fmt.plus = false }は、plusフラグが設定されている場合(例えば%+vのようなフォーマットの場合)に、p.fmt.plusfalseにクリアします。
  • if goSyntax { p.fmt.sharp = false }は、goSyntaxフラグが設定されている場合(例えば%#vのようなGo構文フォーマットの場合)に、p.fmt.sharpfalseにクリアします。

この「クリア」の目的は、後続の基本的なフォーマッタ(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 = oldPlusp.fmt.sharp = oldSharpは、以前に保存しておいたoldPlusoldSharpの値を使って、フラグを元の状態に復元しています。
  • この復元は、その直後に呼び出されるp.handleMethodsのためです。handleMethodsは、フォーマットされる値がFormatterインターフェースなどを実装している場合に、そのカスタムフォーマットロジックを呼び出します。カスタムフォーマットロジックは、fmtパッケージの内部フラグの状態に依存する可能性があるため、handleMethodsが呼び出される前にフラグが元の(呼び出し元が期待する)状態に戻っていることが重要です。

この一連のフラグの保存、クリア、復元のロジックにより、fmtパッケージは、以前の最適化によって生じた%#vのセマンティクス変更を修正し、Go構文での値の表現が常に正しく行われるようにしています。

関連リンク

  • Go言語の変更リスト (CL): https://golang.org/cl/6302048
  • 関連するGo Issue: #3706 (コミットメッセージに記載されていますが、具体的なIssueの内容はコミットメッセージから推測する必要があります)

参考にした情報源リンク