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

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

このコミットは、Go言語の標準ライブラリ fmt パッケージにおける、フォーマット文字列の引数インデックスの評価ロジックの修正に関するものです。特に、幅 (*) や精度 (.) の指定と引数インデックス ([n]) が混在する場合の挙動が、ドキュメントの記述と一致するように変更されました。これにより、fmt.Sprintf などの関数がより予測可能で正確な出力を生成するようになります。

コミット

commit d84132cce77c7826a9b5d55eb385e71e0ad22ade
Author: Rob Pike <r@golang.org>
Date:   Wed May 29 11:29:29 2013 -0400

    fmt: change evalutation of indexed arg to match docs
    The old code put the index before the period in the precision;
    it should be after so it's always before the star, as documented.
    A little trickier to do in one pass but compensated for by more
    tests and catching a couple of other error cases.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/9751044

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

https://github.com/golang/go/commit/d84132cce77c7826a9b5d55eb385e71e0ad22ade

元コミット内容

fmt: change evalutation of indexed arg to match docs The old code put the index before the period in the precision; it should be after so it's always before the star, as documented. A little trickier to do in one pass but compensated for by more tests and catching a couple of other error cases.

R=rsc CC=golang-dev https://golang.org/cl/9751044

変更の背景

Go言語の fmt パッケージは、C言語の printf に似た書式指定文字列を用いた出力機能を提供します。この機能には、引数の順序を明示的に指定できる「引数インデックス」や、幅や精度を動的に指定できる「アスタリスク (*)」などの高度な機能が含まれています。

このコミット以前の fmt パッケージの実装では、引数インデックス ([n])、幅 (* または数値)、精度 (. に続く * または数値) の評価順序に、ドキュメント(fmt パッケージの doc.go)の記述と異なる点がありました。具体的には、精度指定のピリオド (.) の前に引数インデックスが来てしまうケースがあり、これはドキュメントが意図する「アスタリスクの前に常にインデックスが来る」というルールに反していました。

この不一致は、特に複雑なフォーマット文字列を使用する際に、開発者が期待する出力と実際の出力が異なるというバグや混乱の原因となっていました。このコミットは、この挙動の不一致を解消し、ドキュメントに厳密に準拠した評価ロジックを実装することで、fmt パッケージの信頼性と予測可能性を向上させることを目的としています。また、この修正に伴い、より多くのテストケースが追加され、いくつかのエラーケースも捕捉されるようになりました。

前提知識の解説

このコミットを理解するためには、Go言語の fmt パッケージにおける書式指定文字列の基本的な概念と、特に以下の要素に関する知識が必要です。

  1. 書式指定動詞 (Verb): %d (整数), %f (浮動小数点数), %s (文字列) など、値の型と表示形式を指定します。
  2. 引数インデックス (Argument Index): [n] の形式で指定し、書式指定文字列の後に続く引数のうち、n番目の引数を使用することを明示します。例えば、%[2]d は2番目の引数を整数として表示します。これにより、引数の順序を入れ替えたり、同じ引数を複数回使用したりできます。
  3. 幅 (Width): 最小の出力幅を指定します。数値で直接指定するか、アスタリスク (*) を使用して引数から動的に指定できます。アスタリスクを使用する場合、そのアスタリスクに対応する引数は int 型である必要があります。例: %5d (幅5), %*d (引数で幅を指定)。
  4. 精度 (Precision): 浮動小数点数の小数点以下の桁数、文字列の最大文字数などを指定します。ピリオド (.) の後に数値で直接指定するか、アスタリスク (*) を使用して引数から動的に指定できます。アスタリスクを使用する場合、そのアスタリスクに対応する引数は int 型である必要があります。例: %.2f (小数点以下2桁), %.*s (引数で精度を指定)。
  5. フラグ (Flags): +, -, #, (スペース), 0 など、出力の追加的な整形ルールを指定します。

このコミットの核心は、これらの要素、特に引数インデックス、幅、精度の組み合わせがどのように解析され、評価されるかという点にあります。ドキュメントでは、引数インデックスは常にアスタリスク (*) の前に来ると規定されています。例えば、%[3]*.[2]*[1]f のような形式では、[3] が幅のアスタリスク (*) に、[2] が精度のピリオド (.) に続くアスタリスク (*) に、[1] が動詞 (f) に対応する引数を指定します。この順序が正しく解析されることが重要でした。

技術的詳細

fmt パッケージの書式指定文字列の解析は、pp 構造体の doPrintf メソッドで行われます。このメソッドは、書式指定文字列を文字ごとに走査し、% 文字を見つけると、それに続くフラグ、引数インデックス、幅、精度、そして動詞を解析します。

このコミットの主要な変更点は、doPrintf メソッド内の幅と精度の解析ロジックにあります。

  1. afterIndex フラグの導入: doPrintf メソッド内に afterIndex という新しいブーリアン変数 (bool) が導入されました。このフラグは、直前の書式指定要素が引数インデックス ([n]) であったかどうかを追跡します。これにより、引数インデックスの後に続く要素(幅や精度)の解析時に、その文脈を考慮できるようになります。

  2. pp.argNumber 関数の変更: pp.argNumber 関数は、書式指定文字列から引数インデックスを解析する役割を担っています。この関数は、引数インデックスが見つかったかどうかを示す found という新しい戻り値を持つようになりました。これにより、呼び出し元(doPrintf)は、引数インデックスが実際に存在したかどうかを正確に判断できます。

  3. 幅と精度の解析順序の修正:

    • 幅の解析: 以前は、幅のアスタリスク (*) の後に引数インデックスが来る可能性を考慮していましたが、ドキュメントの規定に従い、引数インデックスはアスタリスクの前に来るべきです。修正後、幅のアスタリスクを処理する前に、まず引数インデックスの有無をチェックするようになりました。もし引数インデックスの後に数値の幅が続く場合(例: %[3]2d)、これは不正な形式として BADINDEX エラーを発生させるようになりました。
    • 精度の解析: 精度のピリオド (.) の後に引数インデックスが来る場合(例: %.[2]d)は、ドキュメントに反するため、これも BADINDEX エラーとして扱われるようになりました。精度のピリオドの後にアスタリスク (*) が続く場合、そのアスタリスクに対応する引数インデックスは、ピリオドの前に来るべきです。このロジックが修正され、afterIndex フラグと pp.argNumber の新しい戻り値を利用して、正しい順序での解析が保証されるようになりました。
  4. エラーメッセージの変更: 不正な引数インデックスの指定があった場合のエラーメッセージが、BADARGNUM から BADINDEX に変更されました。これは、より具体的なエラー内容を反映しています。

これらの変更により、fmt パッケージは、書式指定文字列の解析において、引数インデックス、幅、精度の関係をドキュメントに記述された通りに厳密に評価するようになりました。これにより、%[3]*.[2]*[1]f のような複雑な書式指定も正しく解釈され、期待通りの出力が得られるようになります。

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

このコミットで主に変更されたファイルは以下の3つです。

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

    • ドキュメント内の例が修正されました。
      • - fmt.Sprintf("%[3]*[2].*[1]f", 12.0, 2, 6),
      • + fmt.Sprintf("%[3]*.[2]*[1]f", 12.0, 2, 6),
    • エラーメッセージの記述が BADARGNUM から BADINDEX に変更され、新しい不正なインデックス使用例が追加されました。
  2. src/pkg/fmt/fmt_test.go:

    • reorderTests 変数に、新しいテストケースが多数追加されました。これには、幅や精度に引数インデックスを使用するケースや、以前は正しく処理されなかった不正なインデックス使用のケースが含まれます。
    • 特に、%[1].2d%[1]2d, %.[2]d のような、インデックスと幅/精度の組み合わせが不正な場合のテストが追加され、これらが BADINDEX エラーを発生させることを確認しています。
  3. src/pkg/fmt/print.go:

    • badArgNum 定数が badIndexBytes にリネームされました。
    • pp 構造体の goodArgNum フィールドのコメントが更新されました。
    • intFromArg 関数のシグネチャが簡素化されました。
    • pp.argNumber 関数のシグネチャが変更され、引数インデックスが見つかったかどうかを示す found ブーリアン値が追加されました。
    • pp.doPrintf メソッド内で、幅と精度の解析ロジックが大幅に修正されました。
      • afterIndex という新しいブーリアン変数が導入され、直前に引数インデックスが解析されたかを追跡します。
      • 幅 (*) と精度 (.) の解析において、afterIndex の状態と pp.argNumberfound 戻り値を利用して、引数インデックスの評価順序がドキュメントに合うように調整されました。
      • 不正なインデックスの使用パターン(例: %[3]2d%.[2]d)が p.goodArgNum = false と設定され、最終的に badIndexBytes エラーが出力されるようになりました。

コアとなるコードの解説

このコミットの核心的な変更は、src/pkg/fmt/print.go 内の pp.doPrintf メソッドに集中しています。

// src/pkg/fmt/print.go (変更後の一部抜粋)

func (p *pp) doPrintf(format string, a []interface{}) {
	end := len(format)
	argNum := 0         // we process one argument per non-trivial format
	afterIndex := false // previous item in format was an index like [3].
	p.reordered = false
	p.goodArgNum = true // goodArgNum is now goodIndex
	for i := 0; i < end; {
		// ... (前略) ...

		// Do we have an explicit argument index?
		argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))

		// Do we have width?
		if i < end && format[i] == '*' {
			i++
			p.fmt.wid, p.fmt.widPresent, argNum = intFromArg(a, argNum)
			if !p.fmt.widPresent {
				p.buf.Write(badWidthBytes)
			}
			afterIndex = false // '*' consumes the 'afterIndex' state
		} else {
			p.fmt.wid, p.fmt.widPresent, i = parsenum(format, i, end)
			if afterIndex && p.fmt.widPresent { // "%[3]2d" - index followed by numeric width is bad
				p.goodArgNum = false
			}
		}

		// Do we have precision?
		if i+1 < end && format[i] == '.' {
			i++
			if afterIndex { // "%[3].2d" - index followed by '.' is bad
				p.goodArgNum = false
			}
			argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a)) // Check for index after '.'
			if format[i] == '*' {
				i++
				p.fmt.prec, p.fmt.precPresent, argNum = intFromArg(a, argNum)
				if !p.fmt.precPresent {
					p.buf.Write(badPrecBytes)
				}
				afterIndex = false // '*' consumes the 'afterIndex' state
			} else {
				p.fmt.prec, p.fmt.precPresent, i = parsenum(format, i, end)
				if !p.fmt.precPresent {
					p.fmt.prec = 0
					p.fmt.precPresent = true
				}
			}
		}

		if !afterIndex { // If no index was found after precision, check again for a final index before verb
			argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))
		}

		// ... (後略) ...

		if !p.goodArgNum {
			p.buf.WriteByte('%')
			p.add(c)
			p.buf.Write(badIndexBytes) // Use new error message
			continue
		}
	}
}

このコードスニペットは、doPrintf メソッドが書式指定文字列をどのように解析するかを示しています。

  1. afterIndex の利用: p.argNumber が呼び出された後、その戻り値 afterIndex を利用して、直前に引数インデックスが解析されたかどうかを判断します。
  2. 幅の解析ロジック:
    • * で幅が指定される場合、intFromArg を呼び出す前に i++* をスキップし、afterIndexfalse にリセットします。これは、* が引数インデックスの状態を消費するためです。
    • 数値で幅が指定される場合 (parsenum)、もし afterIndextrue であれば(つまり、%[3]2d のようにインデックスの直後に数値の幅が続く場合)、これは不正なパターンとして p.goodArgNumfalse に設定します。
  3. 精度の解析ロジック:
    • . が見つかった場合、まず i++. をスキップします。
    • もし afterIndextrue であれば(つまり、%.[2]d のようにインデックスの直後に . が続く場合)、これは不正なパターンとして p.goodArgNumfalse に設定します。
    • 次に、精度の * の前に引数インデックスが来る可能性を考慮して、再度 p.argNumber を呼び出します。
    • * で精度が指定される場合、intFromArg を呼び出す前に i++* をスキップし、afterIndexfalse にリセットします。
    • 数値で精度が指定される場合 (parsenum) は、通常の解析を行います。
  4. 最終的なインデックスチェック: 幅と精度の解析が完了した後、もし afterIndex がまだ false であれば、動詞の直前に引数インデックスが来る可能性を考慮して、もう一度 p.argNumber を呼び出します。
  5. エラー処理: p.goodArgNumfalse に設定された場合、最終的に badIndexBytes がバッファに書き込まれ、不正な書式指定がエラーとして報告されます。

これらの変更により、fmt パッケージは、書式指定文字列の解析において、引数インデックス、幅、精度の関係をドキュメントに記述された通りに厳密に評価するようになりました。これにより、%[3]*.[2]*[1]f のような複雑な書式指定も正しく解釈され、期待通りの出力が得られるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (fmt パッケージ): https://pkg.go.dev/fmt
  • Go言語のソースコード (src/pkg/fmt/ ディレクトリ): https://github.com/golang/go/tree/master/src/fmt
  • Go言語のコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/9751044 (コミットメッセージに記載されているリンク)
  • Go言語の fmt パッケージの書式指定に関する一般的な情報源 (Web検索結果に基づく)
    • "Go fmt package format string syntax"
    • "Go printf indexed arguments"
    • "Go printf width precision star"
    • "Go fmt badargnum badindex"