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

[インデックス 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"の文字列を数値として解釈しようとする(またはエラーになる)可能性がありました。

コミットメッセージにあるように、「%dStringableな値に使うのはそもそもおかしい」という考え方に基づき、Stringererrorインターフェースは、文字列として解釈されるフォーマット動詞(%s, %q, %v, %x, %X)が指定された場合にのみ適用されるように変更されました。これにより、%dのような数値フォーマット動詞が指定された場合は、Stringererrorインターフェースの実装があっても、その値が数値として扱われるようになります。

また、この変更は、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パッケージのフォーマット優先順位は、おおよそ以下のようになっていました。

  1. Formatterインターフェース(最も細かい制御が可能)
  2. errorインターフェース
  3. Stringerインターフェース
  4. その他の組み込み型やリフレクションによるデフォルトのフォーマット

この優先順位のため、%dのような数値フォーマット動詞が指定された場合でも、値がerrorStringerを実装していると、それらのインターフェースのメソッドが優先的に呼び出されてしまい、期待する数値フォーマットが得られないという問題が発生していました。

技術的詳細

このコミットの技術的な核心は、fmtパッケージの内部処理、特にprint.goファイル内のhandleMethods関数におけるインターフェースのディスパッチロジックの変更にあります。

変更前は、handleMethods関数内で、フォーマット動詞の種類に関わらず、まずerrorインターフェース、次にStringerインターフェースの順でチェックし、もし実装されていればそのメソッドを呼び出して結果を文字列として処理していました。

変更後は、errorおよびStringerインターフェースのチェックと呼び出しが、特定のフォーマット動詞('v', 's', 'x', 'X', 'q')が指定された場合にのみ行われるように条件が追加されました。これらの動詞は、値を文字列として解釈することが自然な文脈です。

具体的には、print.gohandleMethods関数内で、以下の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(真偽値)のような、文字列以外のフォーマット動詞が指定された場合、errorStringerインターフェースが実装されていても、そのメソッドは呼び出されなくなります。代わりに、fmtパッケージは値の基底型(例えばintbool)に基づいてフォーマットを試みます。

この修正は、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つのファイルにわたります。

  1. src/pkg/fmt/doc.go: fmtパッケージのドキュメントファイル。Stringerおよびerrorインターフェースの適用に関するセマンティクスの変更が記述されています。

    • 変更前は、errorインターフェース、次にStringerインターフェースが適用されると記述されていました。
    • 変更後は、「フォーマットが文字列として有効な場合(%s %q %v %x %X)にのみ、以下の2つのルールが適用される」という条件が追加され、その下にerrorStringerのルールがリストアップされています。
    • String()メソッド内での再帰に関する例も、type X intからtype X stringに変更され、Sprintf("%d", int(x))からSprintf("<%s>", string(x))へと修正されています。
  2. 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()メソッドが呼び出されなくなり、テストの意図が変わったためと考えられます。
  3. src/pkg/fmt/print.go: fmtパッケージの主要なフォーマットロジックが実装されているファイル。handleMethods関数内で、errorおよびStringerインターフェースの処理に条件が追加されています。

    • 変更前は、p.fielderrorまたはStringerインターフェースを実装しているかを無条件にチェックし、実装していればそのメソッドを呼び出していました。
    • 変更後は、このチェックと呼び出しが、switch verb文によってverb'v', 's', 'x', 'X', 'q'のいずれかである場合にのみ実行されるように変更されています。

コアとなるコードの解説

src/pkg/fmt/print.gohandleMethods関数は、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
}

この変更により、errorStringerインターフェースのメソッドが呼び出されるのは、verb'v', 's', 'x', 'X', 'q'のいずれかである場合に限定されます。これにより、%dのような数値フォーマット動詞が指定された場合は、これらのインターフェースは無視され、値は数値として適切にフォーマットされるようになります。

この修正は、fmtパッケージのセマンティクスをより直感的で予測可能なものにし、開発者が意図しない文字列変換に遭遇するのを防ぐことを目的としています。また、String()メソッド内での再帰的な呼び出しによる問題も、この条件分岐によって間接的に軽減される可能性があります。例えば、String()メソッドがfmt.Sprintf("%d", someInt)のように数値フォーマットを意図して呼び出している場合、以前は無限ループに陥る可能性がありましたが、この変更によりString()メソッドがそもそも呼び出されなくなるため、問題が回避されます。

関連リンク

参考にした情報源リンク

  • 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"の文字列を数値として解釈しようとする(またはエラーになる)可能性がありました。

コミットメッセージにあるように、「%dStringableな値に使うのはそもそもおかしい」という考え方に基づき、Stringererrorインターフェースは、文字列として解釈されるフォーマット動詞(%s, %q, %v, %x, %X)が指定された場合にのみ適用されるように変更されました。これにより、%dのような数値フォーマット動詞が指定された場合は、Stringererrorインターフェースの実装があっても、その値が数値として扱われるようになります。

また、この変更は、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パッケージのフォーマット優先順位は、おおよそ以下のようになっていました。

  1. Formatterインターフェース(最も細かい制御が可能)
  2. errorインターフェース
  3. Stringerインターフェース
  4. その他の組み込み型やリフレクションによるデフォルトのフォーマット

この優先順位のため、%dのような数値フォーマット動詞が指定された場合でも、値がerrorStringerを実装していると、それらのインターフェースのメソッドが優先的に呼び出されてしまい、期待する数値フォーマットが得られないという問題が発生していました。

Web検索の結果によると、2011年以前のfmtパッケージでは、Stringerインターフェースを実装した型に対して数値フォーマット動詞(例: %d)を使用した場合、通常はString()メソッドは呼び出されず、その型の基底となる数値がフォーマットされていました。しかし、このコミットの背景にあるtime.Month()のケースのように、特定の状況下でStringerが意図せず数値フォーマットに影響を与える、あるいはその挙動が曖昧であるという問題意識があったと考えられます。このコミットは、その曖昧さを解消し、Stringererrorの適用範囲を明確に「文字列フォーマット」に限定することで、より予測可能で一貫性のあるfmtの挙動を目指したものです。

技術的詳細

このコミットの技術的な核心は、fmtパッケージの内部処理、特にprint.goファイル内のhandleMethods関数におけるインターフェースのディスパッチロジックの変更にあります。

変更前は、handleMethods関数内で、フォーマット動詞の種類に関わらず、まずerrorインターフェース、次にStringerインターフェースの順でチェックし、もし実装されていればそのメソッドを呼び出して結果を文字列として処理していました。

変更後は、errorおよびStringerインターフェースのチェックと呼び出しが、特定のフォーマット動詞('v', 's', 'x', 'X', 'q')が指定された場合にのみ行われるように条件が追加されました。これらの動詞は、値を文字列として解釈することが自然な文脈です。

具体的には、print.gohandleMethods関数内で、以下の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(真偽値)のような、文字列以外のフォーマット動詞が指定された場合、errorStringerインターフェースが実装されていても、そのメソッドは呼び出されなくなります。代わりに、fmtパッケージは値の基底型(例えばintbool)に基づいてフォーマットを試みます。

この修正は、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つのファイルにわたります。

  1. src/pkg/fmt/doc.go: fmtパッケージのドキュメントファイル。Stringerおよびerrorインターフェースの適用に関するセマンティクスの変更が記述されています。

    • 変更前は、errorインターフェース、次にStringerインターフェースが適用されると記述されていました。
    • 変更後は、「フォーマットが文字列として有効な場合(%s %q %v %x %X)にのみ、以下の2つのルールが適用される」という条件が追加され、その下にerrorStringerのルールがリストアップされています。
    • String()メソッド内での再帰に関する例も、type X intからtype X stringに変更され、Sprintf("%d", int(x))からSprintf("<%s>", string(x))へと修正されています。
  2. 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()メソッドが呼び出されなくなり、テストの意図が変わったためと考えられます。
  3. src/pkg/fmt/print.go: fmtパッケージの主要なフォーマットロジックが実装されているファイル。handleMethods関数内で、errorおよびStringerインターフェースの処理に条件が追加されています。

    • 変更前は、p.fielderrorまたはStringerインターフェースを実装しているかを無条件にチェックし、実装していればそのメソッドを呼び出していました。
    • 変更後は、このチェックと呼び出しが、switch verb文によってverb'v', 's', 'x', 'X', 'q'のいずれかである場合にのみ実行されるように変更されています。

コアとなるコードの解説

src/pkg/fmt/print.gohandleMethods関数は、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
}

この変更により、errorStringerインターフェースのメソッドが呼び出されるのは、verb'v', 's', 'x', 'X', 'q'のいずれかである場合に限定されます。これにより、%dのような数値フォーマット動詞が指定された場合は、これらのインターフェースは無視され、値は数値として適切にフォーマットされるようになります。

この修正は、fmtパッケージのセマンティクスをより直感的で予測可能なものにし、開発者が意図しない文字列変換に遭遇するのを防ぐことを目的としています。また、String()メソッド内での再帰的な呼び出しによる問題も、この条件分岐によって間接的に軽減される可能性があります。例えば、String()メソッドがfmt.Sprintf("%d", someInt)のように数値フォーマットを意図して呼び出している場合、以前は無限ループに陥る可能性がありましたが、この変更によりString()メソッドがそもそも呼び出されなくなるため、問題が回避されます。

関連リンク

参考にした情報源リンク

  • 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"