[インデックス 10619] ファイルの概要
このコミットは、Go言語のfmt
パッケージにおけるフォーマットのセマンティクスを微調整するものです。具体的には、Stringer
インターフェースやerror
インターフェースの実装が、文字列フォーマット(%s
, %q
, %v
, %x
, %X
)でのみ適用されるように変更されました。これにより、数値フォーマット(%d
など)でStringer
を実装した型を扱った場合に、その型のString()
メソッドが呼び出されず、数値として正しくフォーマットされるようになります。
コミット
commit 2ed57a8cd86cec36b8370fb16d450e5a29a9375f
Author: Rob Pike <r@golang.org>
Date: Mon Dec 5 16:45:51 2011 -0800
fmt: only use Stringer or Error for strings
This is a slight change to fmt's semantics, but means that if you use
%d to print an integer with a Stringable value, it will print as an integer.
This came up because Time.Month() couldn't cleanly print as an integer
rather than a name. Using %d on Stringables is silly anyway, so there
should be no effect outside the fmt tests.
As a mild bonus, certain recursive failures of String methods
will also be avoided this way.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5453053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2ed57a8cd86cec36b8370fb16d450e5a29a9375f
元コミット内容
fmt: only use Stringer or Error for strings
This is a slight change to fmt's semantics, but means that if you use
%d to print an integer with a Stringable value, it will print as an integer.
This came up because Time.Month() couldn't cleanly print as an integer
rather than a name. Using %d on Stringables is silly anyway, so there
should be no effect outside the fmt tests.
As a mild bonus, certain recursive failures of String methods
will also be avoided this way.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5453053
変更の背景
この変更の主な背景は、fmt
パッケージが値をフォーマットする際のStringer
およびerror
インターフェースの適用範囲に関するセマンティクスの曖昧さ、およびそれに起因する予期せぬ挙動を解消することにあります。
具体的には、time.Month()
のような、数値と文字列の両方の表現を持つ型が問題となっていました。time.Month
型はStringer
インターフェースを実装しており、fmt.Printf("%s", time.January)
のように文字列としてフォーマットすると"January"と出力されます。しかし、fmt.Printf("%d", time.January)
のように数値としてフォーマットしようとした場合、以前のfmt
の挙動ではStringer
インターフェースが優先され、String()
メソッドが呼び出されてしまい、数値として期待される"1"ではなく、"January"の文字列を数値として解釈しようとする(またはエラーになる)可能性がありました。
コミットメッセージにあるように、「%d
をStringable
な値に使うのはそもそもおかしい」という考え方に基づき、Stringer
やerror
インターフェースは、文字列として解釈されるフォーマット動詞(%s
, %q
, %v
, %x
, %X
)が指定された場合にのみ適用されるように変更されました。これにより、%d
のような数値フォーマット動詞が指定された場合は、Stringer
やerror
インターフェースの実装があっても、その値が数値として扱われるようになります。
また、この変更は、String()
メソッドがfmt.Sprintf
などを再帰的に呼び出す場合に発生する可能性のある無限ループやスタックオーバーフローといった再帰的な失敗を回避する副次的な効果ももたらします。
前提知識の解説
このコミットを理解するためには、Go言語の以下の基本的な概念とfmt
パッケージの動作に関する知識が必要です。
Go言語のインターフェース
Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを満たすと見なされます。Goのインターフェースは「暗黙的」であり、JavaやC#のようにimplements
キーワードを使って明示的にインターフェースを実装することを宣言する必要はありません。
fmt
パッケージ
fmt
パッケージは、Go言語においてフォーマットされたI/O(入力/出力)を実装するための機能を提供します。C言語のprintf
/scanf
に似た関数群を提供し、様々な型の値を文字列に変換して出力したり、文字列から値を解析したりすることができます。
fmt
パッケージの主要な関数には以下のようなものがあります。
fmt.Print
,fmt.Println
,fmt.Printf
: 標準出力への出力fmt.Sprint
,fmt.Sprintln
,fmt.Sprintf
: 文字列へのフォーマットfmt.Fprint
,fmt.Fprintln
,fmt.Fprintf
:io.Writer
への出力
フォーマット動詞 (Verbs)
fmt
パッケージでは、Printf
系の関数で値をどのようにフォーマットするかを制御するために「フォーマット動詞(verbs)」を使用します。フォーマット動詞は%
記号に続く文字で指定され、例えば以下のようなものがあります。
%v
: 値をデフォルトのフォーマットで出力します。%s
: 文字列として出力します。%d
: 10進数として出力します。%q
: Goの構文に沿った引用符付き文字列として出力します。%x
,%X
: 16進数として出力します。%t
: 真偽値として出力します。%p
: ポインタアドレスを16進数で出力します。
Stringer
インターフェース
fmt
パッケージは、特定のインターフェースを実装している型に対して特別なフォーマットルールを適用します。その一つがStringer
インターフェースです。
type Stringer interface {
String() string
}
任意の型がこのString()
メソッドを実装している場合、fmt
パッケージはその型の値を文字列としてフォーマットする際に、自動的にString()
メソッドを呼び出してその戻り値を使用します。これは、カスタム型を人間が読める形式で出力する際に非常に便利です。
error
インターフェース
もう一つ、fmt
パッケージが特別扱いするインターフェースがerror
インターフェースです。
type error interface {
Error() string
}
Go言語のエラー処理において中心的な役割を果たすインターフェースで、エラーを表す型はこのError()
メソッドを実装します。fmt
パッケージは、error
インターフェースを実装する値をフォーマットする際に、Error()
メソッドを呼び出してその戻り値を使用します。
fmt
パッケージのフォーマット優先順位(変更前)
このコミット以前のfmt
パッケージのフォーマット優先順位は、おおよそ以下のようになっていました。
Formatter
インターフェース(最も細かい制御が可能)error
インターフェースStringer
インターフェース- その他の組み込み型やリフレクションによるデフォルトのフォーマット
この優先順位のため、%d
のような数値フォーマット動詞が指定された場合でも、値がerror
やStringer
を実装していると、それらのインターフェースのメソッドが優先的に呼び出されてしまい、期待する数値フォーマットが得られないという問題が発生していました。
技術的詳細
このコミットの技術的な核心は、fmt
パッケージの内部処理、特にprint.go
ファイル内のhandleMethods
関数におけるインターフェースのディスパッチロジックの変更にあります。
変更前は、handleMethods
関数内で、フォーマット動詞の種類に関わらず、まずerror
インターフェース、次にStringer
インターフェースの順でチェックし、もし実装されていればそのメソッドを呼び出して結果を文字列として処理していました。
変更後は、error
およびStringer
インターフェースのチェックと呼び出しが、特定のフォーマット動詞('v'
, 's'
, 'x'
, 'X'
, 'q'
)が指定された場合にのみ行われるように条件が追加されました。これらの動詞は、値を文字列として解釈することが自然な文脈です。
具体的には、print.go
のhandleMethods
関数内で、以下のswitch verb
文が追加されました。
switch verb {
case 'v', 's', 'x', 'X', 'q':
// Is it an error or Stringer?
// The duplication in the bodies is necessary:
// setting wasString and handled, and deferring catchPanic,
// must happen before calling the method.
switch v := p.field.(type) {
case error:
wasString = false
handled = true
defer p.catchPanic(p.field, verb)
p.printField(v.Error(), verb, plus, false, depth)
return
case Stringer:
wasString = false
handled = true
defer p.catchPanic(p.field, verb)
p.printField(v.String(), verb, plus, false, depth)
return
}
}
この変更により、例えば%d
(10進数)や%t
(真偽値)のような、文字列以外のフォーマット動詞が指定された場合、error
やStringer
インターフェースが実装されていても、そのメソッドは呼び出されなくなります。代わりに、fmt
パッケージは値の基底型(例えばint
やbool
)に基づいてフォーマットを試みます。
この修正は、fmt
パッケージのドキュメント(doc.go
)にも反映され、error
およびStringer
インターフェースの適用が「文字列として有効なフォーマット(%s %q %v %x %X
)の場合にのみ適用される」という新しいルールが明記されました。
また、fmt_test.go
には、time.Month()
の例が追加され、%s
でフォーマットすると"January"、%d
でフォーマットすると"1"となることがテストで確認されています。これは、この変更が意図した挙動であることを示しています。
さらに、String()
メソッド内での再帰的なSprintf
呼び出しに関するドキュメントの例も更新されました。以前はtype X int
の例でSprintf("%d", int(x))
とキャストして再帰を避ける方法を示していましたが、変更後はtype X string
の例でSprintf("<%s>", string(x))
と、文字列フォーマット動詞と文字列へのキャストを組み合わせて再帰を避ける方法が示されています。これは、Stringer
が文字列フォーマットにのみ適用されるという新しいセマンティクスを反映したものです。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下の3つのファイルにわたります。
-
src/pkg/fmt/doc.go
:fmt
パッケージのドキュメントファイル。Stringer
およびerror
インターフェースの適用に関するセマンティクスの変更が記述されています。- 変更前は、
error
インターフェース、次にStringer
インターフェースが適用されると記述されていました。 - 変更後は、「フォーマットが文字列として有効な場合(
%s %q %v %x %X
)にのみ、以下の2つのルールが適用される」という条件が追加され、その下にerror
とStringer
のルールがリストアップされています。 String()
メソッド内での再帰に関する例も、type X int
からtype X string
に変更され、Sprintf("%d", int(x))
からSprintf("<%s>", string(x))
へと修正されています。
- 変更前は、
-
src/pkg/fmt/fmt_test.go
:fmt
パッケージのテストファイル。新しいセマンティクスを検証するためのテストケースが追加・修正されています。fmttests
変数内のI(23)
(Stringer
を実装したカスタム型)に対する%d
フォーマットの期待値が、%!d(string=<23>)
から23
に変更されています。これは、Stringer
が数値フォーマットに適用されなくなったことを示します。time.Time{}.Month()
に対する%s
と%d
のテストケースが追加されています。%s
では"January"、%d
では"1"が期待されるようになり、これがこの変更の主要な動機の一つであったことがわかります。panictests
変数内のPanic
型に対する%d
フォーマットのテストケースが%s
フォーマットに変更されています。これは、Panic
型がStringer
を実装しているため、数値フォーマットではString()
メソッドが呼び出されなくなり、テストの意図が変わったためと考えられます。
-
src/pkg/fmt/print.go
:fmt
パッケージの主要なフォーマットロジックが実装されているファイル。handleMethods
関数内で、error
およびStringer
インターフェースの処理に条件が追加されています。- 変更前は、
p.field
がerror
またはStringer
インターフェースを実装しているかを無条件にチェックし、実装していればそのメソッドを呼び出していました。 - 変更後は、このチェックと呼び出しが、
switch verb
文によってverb
が'v'
,'s'
,'x'
,'X'
,'q'
のいずれかである場合にのみ実行されるように変更されています。
- 変更前は、
コアとなるコードの解説
src/pkg/fmt/print.go
のhandleMethods
関数は、fmt
パッケージが値をフォーマットする際に、その値が特定のインターフェース(Formatter
, error
, Stringer
, GoStringer
など)を実装しているかどうかをチェックし、適切なメソッドを呼び出す役割を担っています。
変更前のコードは以下のようになっていました(簡略化)。
func (p *pp) handleMethods(verb rune, plus, goSyntax bool, depth int) (wasString bool, handled bool) {
// ... Formatter interface handling ...
// Is it an error or Stringer?
switch v := p.field.(type) {
case error:
// Call v.Error()
p.printField(v.Error(), verb, plus, false, depth)
return
case Stringer:
// Call v.String()
p.printField(v.String(), verb, plus, false, depth)
return
}
handled = false
return
}
このロジックでは、verb
(フォーマット動詞)が何であるかに関わらず、error
またはStringer
インターフェースが実装されていれば、そのメソッドが呼び出されていました。これが、time.Month()
の例で%d
を使っても数値ではなく文字列が返される原因でした。
変更後のコードは以下のようになります。
func (p *pp) handleMethods(verb rune, plus, goSyntax bool, depth int) (wasString bool, handled bool) {
// ... Formatter interface handling ...
// If a string is acceptable according to the format, see if
// the value satisfies one of the string-valued interfaces.
// Println etc. set verb to %v, which is "stringable".
switch verb {
case 'v', 's', 'x', 'X', 'q': // ここで文字列として解釈されるフォーマット動詞に限定
// Is it an error or Stringer?
switch v := p.field.(type) {
case error:
// Call v.Error()
p.printField(v.Error(), verb, plus, false, depth)
return
case Stringer:
// Call v.String()
p.printField(v.String(), verb, plus, false, depth)
return
}
}
handled = false
return
}
この変更により、error
やStringer
インターフェースのメソッドが呼び出されるのは、verb
が'v'
, 's'
, 'x'
, 'X'
, 'q'
のいずれかである場合に限定されます。これにより、%d
のような数値フォーマット動詞が指定された場合は、これらのインターフェースは無視され、値は数値として適切にフォーマットされるようになります。
この修正は、fmt
パッケージのセマンティクスをより直感的で予測可能なものにし、開発者が意図しない文字列変換に遭遇するのを防ぐことを目的としています。また、String()
メソッド内での再帰的な呼び出しによる問題も、この条件分岐によって間接的に軽減される可能性があります。例えば、String()
メソッドがfmt.Sprintf("%d", someInt)
のように数値フォーマットを意図して呼び出している場合、以前は無限ループに陥る可能性がありましたが、この変更によりString()
メソッドがそもそも呼び出されなくなるため、問題が回避されます。
関連リンク
- Go言語の
fmt
パッケージのドキュメント: https://pkg.go.dev/fmt - Go言語の
Stringer
インターフェースに関する公式ドキュメント: https://pkg.go.dev/fmt#Stringer - Go言語の
error
インターフェースに関する公式ドキュメント: https://pkg.go.dev/builtin#error
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(
src/pkg/fmt/
ディレクトリ) - コミットメッセージと関連するコードレビュー(
https://golang.org/cl/5453053
) - Go言語のインターフェースに関する一般的な解説記事
- Go言語の
fmt
パッケージのフォーマット動詞に関する解説記事
[インデックス 10619] ファイルの概要
このコミットは、Go言語のfmt
パッケージにおけるフォーマットのセマンティクスを微調整するものです。具体的には、Stringer
インターフェースやerror
インターフェースの実装が、文字列フォーマット(%s
, %q
, %v
, %x
, %X
)でのみ適用されるように変更されました。これにより、数値フォーマット(%d
など)でStringer
を実装した型を扱った場合に、その型のString()
メソッドが呼び出されず、数値として正しくフォーマットされるようになります。
コミット
commit 2ed57a8cd86cec36b8370fb16d450e5a29a9375f
Author: Rob Pike <r@golang.org>
Date: Mon Dec 5 16:45:51 2011 -0800
fmt: only use Stringer or Error for strings
This is a slight change to fmt's semantics, but means that if you use
%d to print an integer with a Stringable value, it will print as an integer.
This came up because Time.Month() couldn't cleanly print as an integer
rather than a name. Using %d on Stringables is silly anyway, so there
should be no effect outside the fmt tests.
As a mild bonus, certain recursive failures of String methods
will also be avoided this way.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5453053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2ed57a8cd86cec36b8370fb16d450e5a29a9375f
元コミット内容
fmt: only use Stringer or Error for strings
This is a slight change to fmt's semantics, but means that if you use
%d to print an integer with a Stringable value, it will print as an integer.
This came up because Time.Month() couldn't cleanly print as an integer
rather than a name. Using %d on Stringables is silly anyway, so there
should be no effect outside the fmt tests.
As a mild bonus, certain recursive failures of String methods
will also be avoided this way.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5453053
変更の背景
この変更の主な背景は、fmt
パッケージが値をフォーマットする際のStringer
およびerror
インターフェースの適用範囲に関するセマンティクスの曖昧さ、およびそれに起因する予期せぬ挙動を解消することにあります。
具体的には、time.Month()
のような、数値と文字列の両方の表現を持つ型が問題となっていました。time.Month
型はStringer
インターフェースを実装しており、fmt.Printf("%s", time.January)
のように文字列としてフォーマットすると"January"と出力されます。しかし、fmt.Printf("%d", time.January)
のように数値としてフォーマットしようとした場合、以前のfmt
の挙動ではStringer
インターフェースが優先され、String()
メソッドが呼び出されてしまい、数値として期待される"1"ではなく、"January"の文字列を数値として解釈しようとする(またはエラーになる)可能性がありました。
コミットメッセージにあるように、「%d
をStringable
な値に使うのはそもそもおかしい」という考え方に基づき、Stringer
やerror
インターフェースは、文字列として解釈されるフォーマット動詞(%s
, %q
, %v
, %x
, %X
)が指定された場合にのみ適用されるように変更されました。これにより、%d
のような数値フォーマット動詞が指定された場合は、Stringer
やerror
インターフェースの実装があっても、その値が数値として扱われるようになります。
また、この変更は、String()
メソッドがfmt.Sprintf
などを再帰的に呼び出す場合に発生する可能性のある無限ループやスタックオーバーフローといった再帰的な失敗を回避する副次的な効果ももたらします。
前提知識の解説
このコミットを理解するためには、Go言語の以下の基本的な概念とfmt
パッケージの動作に関する知識が必要です。
Go言語のインターフェース
Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを満たすと見なされます。Goのインターフェースは「暗黙的」であり、JavaやC#のようにimplements
キーワードを使って明示的にインターフェースを実装することを宣言する必要はありません。
fmt
パッケージ
fmt
パッケージは、Go言語においてフォーマットされたI/O(入力/出力)を実装するための機能を提供します。C言語のprintf
/scanf
に似た関数群を提供し、様々な型の値を文字列に変換して出力したり、文字列から値を解析したりすることができます。
fmt
パッケージの主要な関数には以下のようなものがあります。
fmt.Print
,fmt.Println
,fmt.Printf
: 標準出力への出力fmt.Sprint
,fmt.Sprintln
,fmt.Sprintf
: 文字列へのフォーマットfmt.Fprint
,fmt.Fprintln
,fmt.Fprintf
:io.Writer
への出力
フォーマット動詞 (Verbs)
fmt
パッケージでは、Printf
系の関数で値をどのようにフォーマットするかを制御するために「フォーマット動詞(verbs)」を使用します。フォーマット動詞は%
記号に続く文字で指定され、例えば以下のようなものがあります。
%v
: 値をデフォルトのフォーマットで出力します。%s
: 文字列として出力します。%d
: 10進数として出力します。%q
: Goの構文に沿った引用符付き文字列として出力します。%x
,%X
: 16進数として出力します。%t
: 真偽値として出力します。%p
: ポインタアドレスを16進数で出力します。
Stringer
インターフェース
fmt
パッケージは、特定のインターフェースを実装している型に対して特別なフォーマットルールを適用します。その一つがStringer
インターフェースです。
type Stringer interface {
String() string
}
任意の型がこのString()
メソッドを実装している場合、fmt
パッケージはその型の値を文字列としてフォーマットする際に、自動的にString()
メソッドを呼び出してその戻り値を使用します。これは、カスタム型を人間が読める形式で出力する際に非常に便利です。
error
インターフェース
もう一つ、fmt
パッケージが特別扱いするインターフェースがerror
インターフェースです。
type error interface {
Error() string
}
Go言語のエラー処理において中心的な役割を果たすインターフェースで、エラーを表す型はこのError()
メソッドを実装します。fmt
パッケージは、error
インターフェースを実装する値をフォーマットする際に、Error()
メソッドを呼び出してその戻り値を使用します。
fmt
パッケージのフォーマット優先順位(変更前)
このコミット以前のfmt
パッケージのフォーマット優先順位は、おおよそ以下のようになっていました。
Formatter
インターフェース(最も細かい制御が可能)error
インターフェースStringer
インターフェース- その他の組み込み型やリフレクションによるデフォルトのフォーマット
この優先順位のため、%d
のような数値フォーマット動詞が指定された場合でも、値がerror
やStringer
を実装していると、それらのインターフェースのメソッドが優先的に呼び出されてしまい、期待する数値フォーマットが得られないという問題が発生していました。
Web検索の結果によると、2011年以前のfmt
パッケージでは、Stringer
インターフェースを実装した型に対して数値フォーマット動詞(例: %d
)を使用した場合、通常はString()
メソッドは呼び出されず、その型の基底となる数値がフォーマットされていました。しかし、このコミットの背景にあるtime.Month()
のケースのように、特定の状況下でStringer
が意図せず数値フォーマットに影響を与える、あるいはその挙動が曖昧であるという問題意識があったと考えられます。このコミットは、その曖昧さを解消し、Stringer
やerror
の適用範囲を明確に「文字列フォーマット」に限定することで、より予測可能で一貫性のあるfmt
の挙動を目指したものです。
技術的詳細
このコミットの技術的な核心は、fmt
パッケージの内部処理、特にprint.go
ファイル内のhandleMethods
関数におけるインターフェースのディスパッチロジックの変更にあります。
変更前は、handleMethods
関数内で、フォーマット動詞の種類に関わらず、まずerror
インターフェース、次にStringer
インターフェースの順でチェックし、もし実装されていればそのメソッドを呼び出して結果を文字列として処理していました。
変更後は、error
およびStringer
インターフェースのチェックと呼び出しが、特定のフォーマット動詞('v'
, 's'
, 'x'
, 'X'
, 'q'
)が指定された場合にのみ行われるように条件が追加されました。これらの動詞は、値を文字列として解釈することが自然な文脈です。
具体的には、print.go
のhandleMethods
関数内で、以下のswitch verb
文が追加されました。
switch verb {
case 'v', 's', 'x', 'X', 'q':
// Is it an error or Stringer?
// The duplication in the bodies is necessary:
// setting wasString and handled, and deferring catchPanic,
// must happen before calling the method.
switch v := p.field.(type) {
case error:
wasString = false
handled = true
defer p.catchPanic(p.field, verb)
p.printField(v.Error(), verb, plus, false, depth)
return
case Stringer:
wasString = false
handled = true
defer p.catchPanic(p.field, verb)
p.printField(v.String(), verb, plus, false, depth)
return
}
}
この変更により、例えば%d
(10進数)や%t
(真偽値)のような、文字列以外のフォーマット動詞が指定された場合、error
やStringer
インターフェースが実装されていても、そのメソッドは呼び出されなくなります。代わりに、fmt
パッケージは値の基底型(例えばint
やbool
)に基づいてフォーマットを試みます。
この修正は、fmt
パッケージのドキュメント(doc.go
)にも反映され、error
およびStringer
インターフェースの適用が「文字列として有効なフォーマット(%s %q %v %x %X
)の場合にのみ適用される」という新しいルールが明記されました。
また、fmt_test.go
には、time.Month()
の例が追加され、%s
でフォーマットすると"January"、%d
でフォーマットすると"1"となることがテストで確認されています。これは、この変更が意図した挙動であることを示しています。
さらに、String()
メソッド内での再帰的なSprintf
呼び出しに関するドキュメントの例も更新されました。以前はtype X int
の例でSprintf("%d", int(x))
とキャストして再帰を避ける方法を示していましたが、変更後はtype X string
の例でSprintf("<%s>", string(x))
と、文字列フォーマット動詞と文字列へのキャストを組み合わせて再帰を避ける方法が示されています。これは、Stringer
が文字列フォーマットにのみ適用されるという新しいセマンティクスを反映したものです。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下の3つのファイルにわたります。
-
src/pkg/fmt/doc.go
:fmt
パッケージのドキュメントファイル。Stringer
およびerror
インターフェースの適用に関するセマンティクスの変更が記述されています。- 変更前は、
error
インターフェース、次にStringer
インターフェースが適用されると記述されていました。 - 変更後は、「フォーマットが文字列として有効な場合(
%s %q %v %x %X
)にのみ、以下の2つのルールが適用される」という条件が追加され、その下にerror
とStringer
のルールがリストアップされています。 String()
メソッド内での再帰に関する例も、type X int
からtype X string
に変更され、Sprintf("%d", int(x))
からSprintf("<%s>", string(x))
へと修正されています。
- 変更前は、
-
src/pkg/fmt/fmt_test.go
:fmt
パッケージのテストファイル。新しいセマンティクスを検証するためのテストケースが追加・修正されています。fmttests
変数内のI(23)
(Stringer
を実装したカスタム型)に対する%d
フォーマットの期待値が、%!d(string=<23>)
から23
に変更されています。これは、Stringer
が数値フォーマットに適用されなくなったことを示します。time.Time{}.Month()
に対する%s
と%d
のテストケースが追加されています。%s
では"January"、%d
では"1"が期待されるようになり、これがこの変更の主要な動機の一つであったことがわかります。panictests
変数内のPanic
型に対する%d
フォーマットのテストケースが%s
フォーマットに変更されています。これは、Panic
型がStringer
を実装しているため、数値フォーマットではString()
メソッドが呼び出されなくなり、テストの意図が変わったためと考えられます。
-
src/pkg/fmt/print.go
:fmt
パッケージの主要なフォーマットロジックが実装されているファイル。handleMethods
関数内で、error
およびStringer
インターフェースの処理に条件が追加されています。- 変更前は、
p.field
がerror
またはStringer
インターフェースを実装しているかを無条件にチェックし、実装していればそのメソッドを呼び出していました。 - 変更後は、このチェックと呼び出しが、
switch verb
文によってverb
が'v'
,'s'
,'x'
,'X'
,'q'
のいずれかである場合にのみ実行されるように変更されています。
- 変更前は、
コアとなるコードの解説
src/pkg/fmt/print.go
のhandleMethods
関数は、fmt
パッケージが値をフォーマットする際に、その値が特定のインターフェース(Formatter
, error
, Stringer
, GoStringer
など)を実装しているかどうかをチェックし、適切なメソッドを呼び出す役割を担っています。
変更前のコードは以下のようになっていました(簡略化)。
func (p *pp) handleMethods(verb rune, plus, goSyntax bool, depth int) (wasString bool, handled bool) {
// ... Formatter interface handling ...
// Is it an error or Stringer?
switch v := p.field.(type) {
case error:
// Call v.Error()
p.printField(v.Error(), verb, plus, false, depth)
return
case Stringer:
// Call v.String()
p.printField(v.String(), verb, plus, false, depth)
return
}
handled = false
return
}
このロジックでは、verb
(フォーマット動詞)が何であるかに関わらず、error
またはStringer
インターフェースが実装されていれば、そのメソッドが呼び出されていました。これが、time.Month()
の例で%d
を使っても数値ではなく文字列が返される原因でした。
変更後のコードは以下のようになります。
func (p *pp) handleMethods(verb rune, plus, goSyntax bool, depth int) (wasString bool, handled bool) {
// ... Formatter interface handling ...
// If a string is acceptable according to the format, see if
// the value satisfies one of the string-valued interfaces.
// Println etc. set verb to %v, which is "stringable".
switch verb {
case 'v', 's', 'x', 'X', 'q': // ここで文字列として解釈されるフォーマット動詞に限定
// Is it an error or Stringer?
switch v := p.field.(type) {
case error:
// Call v.Error()
p.printField(v.Error(), verb, plus, false, depth)
return
case Stringer:
// Call v.String()
p.printField(v.String(), verb, plus, false, depth)
return
}
}
handled = false
return
}
この変更により、error
やStringer
インターフェースのメソッドが呼び出されるのは、verb
が'v'
, 's'
, 'x'
, 'X'
, 'q'
のいずれかである場合に限定されます。これにより、%d
のような数値フォーマット動詞が指定された場合は、これらのインターフェースは無視され、値は数値として適切にフォーマットされるようになります。
この修正は、fmt
パッケージのセマンティクスをより直感的で予測可能なものにし、開発者が意図しない文字列変換に遭遇するのを防ぐことを目的としています。また、String()
メソッド内での再帰的な呼び出しによる問題も、この条件分岐によって間接的に軽減される可能性があります。例えば、String()
メソッドがfmt.Sprintf("%d", someInt)
のように数値フォーマットを意図して呼び出している場合、以前は無限ループに陥る可能性がありましたが、この変更によりString()
メソッドがそもそも呼び出されなくなるため、問題が回避されます。
関連リンク
- Go言語の
fmt
パッケージのドキュメント: https://pkg.go.dev/fmt - Go言語の
Stringer
インターフェースに関する公式ドキュメント: https://pkg.go.dev/fmt#Stringer - Go言語の
error
インターフェースに関する公式ドキュメント: https://pkg.go.dev/builtin#error
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(
src/pkg/fmt/
ディレクトリ) - コミットメッセージと関連するコードレビュー(
https://golang.org/cl/5453053
) - Go言語のインターフェースに関する一般的な解説記事
- Go言語の
fmt
パッケージのフォーマット動詞に関する解説記事 - Web検索: "Go fmt package Stringer interface behavior with numeric verbs before 2011"