[インデックス 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 パッケージにおける書式指定文字列の基本的な概念と、特に以下の要素に関する知識が必要です。
- 書式指定動詞 (Verb):
%d(整数),%f(浮動小数点数),%s(文字列) など、値の型と表示形式を指定します。 - 引数インデックス (Argument Index):
[n]の形式で指定し、書式指定文字列の後に続く引数のうち、n番目の引数を使用することを明示します。例えば、%[2]dは2番目の引数を整数として表示します。これにより、引数の順序を入れ替えたり、同じ引数を複数回使用したりできます。 - 幅 (Width): 最小の出力幅を指定します。数値で直接指定するか、アスタリスク (
*) を使用して引数から動的に指定できます。アスタリスクを使用する場合、そのアスタリスクに対応する引数はint型である必要があります。例:%5d(幅5),%*d(引数で幅を指定)。 - 精度 (Precision): 浮動小数点数の小数点以下の桁数、文字列の最大文字数などを指定します。ピリオド (
.) の後に数値で直接指定するか、アスタリスク (*) を使用して引数から動的に指定できます。アスタリスクを使用する場合、そのアスタリスクに対応する引数はint型である必要があります。例:%.2f(小数点以下2桁),%.*s(引数で精度を指定)。 - フラグ (Flags):
+,-,#,(スペース),0など、出力の追加的な整形ルールを指定します。
このコミットの核心は、これらの要素、特に引数インデックス、幅、精度の組み合わせがどのように解析され、評価されるかという点にあります。ドキュメントでは、引数インデックスは常にアスタリスク (*) の前に来ると規定されています。例えば、%[3]*.[2]*[1]f のような形式では、[3] が幅のアスタリスク (*) に、[2] が精度のピリオド (.) に続くアスタリスク (*) に、[1] が動詞 (f) に対応する引数を指定します。この順序が正しく解析されることが重要でした。
技術的詳細
fmt パッケージの書式指定文字列の解析は、pp 構造体の doPrintf メソッドで行われます。このメソッドは、書式指定文字列を文字ごとに走査し、% 文字を見つけると、それに続くフラグ、引数インデックス、幅、精度、そして動詞を解析します。
このコミットの主要な変更点は、doPrintf メソッド内の幅と精度の解析ロジックにあります。
-
afterIndexフラグの導入:doPrintfメソッド内にafterIndexという新しいブーリアン変数 (bool) が導入されました。このフラグは、直前の書式指定要素が引数インデックス ([n]) であったかどうかを追跡します。これにより、引数インデックスの後に続く要素(幅や精度)の解析時に、その文脈を考慮できるようになります。 -
pp.argNumber関数の変更:pp.argNumber関数は、書式指定文字列から引数インデックスを解析する役割を担っています。この関数は、引数インデックスが見つかったかどうかを示すfoundという新しい戻り値を持つようになりました。これにより、呼び出し元(doPrintf)は、引数インデックスが実際に存在したかどうかを正確に判断できます。 -
幅と精度の解析順序の修正:
- 幅の解析: 以前は、幅のアスタリスク (
*) の後に引数インデックスが来る可能性を考慮していましたが、ドキュメントの規定に従い、引数インデックスはアスタリスクの前に来るべきです。修正後、幅のアスタリスクを処理する前に、まず引数インデックスの有無をチェックするようになりました。もし引数インデックスの後に数値の幅が続く場合(例:%[3]2d)、これは不正な形式としてBADINDEXエラーを発生させるようになりました。 - 精度の解析: 精度のピリオド (
.) の後に引数インデックスが来る場合(例:%.[2]d)は、ドキュメントに反するため、これもBADINDEXエラーとして扱われるようになりました。精度のピリオドの後にアスタリスク (*) が続く場合、そのアスタリスクに対応する引数インデックスは、ピリオドの前に来るべきです。このロジックが修正され、afterIndexフラグとpp.argNumberの新しい戻り値を利用して、正しい順序での解析が保証されるようになりました。
- 幅の解析: 以前は、幅のアスタリスク (
-
エラーメッセージの変更: 不正な引数インデックスの指定があった場合のエラーメッセージが、
BADARGNUMからBADINDEXに変更されました。これは、より具体的なエラー内容を反映しています。
これらの変更により、fmt パッケージは、書式指定文字列の解析において、引数インデックス、幅、精度の関係をドキュメントに記述された通りに厳密に評価するようになりました。これにより、%[3]*.[2]*[1]f のような複雑な書式指定も正しく解釈され、期待通りの出力が得られるようになります。
コアとなるコードの変更箇所
このコミットで主に変更されたファイルは以下の3つです。
-
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に変更され、新しい不正なインデックス使用例が追加されました。
- ドキュメント内の例が修正されました。
-
src/pkg/fmt/fmt_test.go:reorderTests変数に、新しいテストケースが多数追加されました。これには、幅や精度に引数インデックスを使用するケースや、以前は正しく処理されなかった不正なインデックス使用のケースが含まれます。- 特に、
%[1].2dや%[1]2d,%.[2]dのような、インデックスと幅/精度の組み合わせが不正な場合のテストが追加され、これらがBADINDEXエラーを発生させることを確認しています。
-
src/pkg/fmt/print.go:badArgNum定数がbadIndexBytesにリネームされました。pp構造体のgoodArgNumフィールドのコメントが更新されました。intFromArg関数のシグネチャが簡素化されました。pp.argNumber関数のシグネチャが変更され、引数インデックスが見つかったかどうかを示すfoundブーリアン値が追加されました。pp.doPrintfメソッド内で、幅と精度の解析ロジックが大幅に修正されました。afterIndexという新しいブーリアン変数が導入され、直前に引数インデックスが解析されたかを追跡します。- 幅 (
*) と精度 (.) の解析において、afterIndexの状態とpp.argNumberのfound戻り値を利用して、引数インデックスの評価順序がドキュメントに合うように調整されました。 - 不正なインデックスの使用パターン(例:
%[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 メソッドが書式指定文字列をどのように解析するかを示しています。
afterIndexの利用:p.argNumberが呼び出された後、その戻り値afterIndexを利用して、直前に引数インデックスが解析されたかどうかを判断します。- 幅の解析ロジック:
*で幅が指定される場合、intFromArgを呼び出す前にi++で*をスキップし、afterIndexをfalseにリセットします。これは、*が引数インデックスの状態を消費するためです。- 数値で幅が指定される場合 (
parsenum)、もしafterIndexがtrueであれば(つまり、%[3]2dのようにインデックスの直後に数値の幅が続く場合)、これは不正なパターンとしてp.goodArgNumをfalseに設定します。
- 精度の解析ロジック:
.が見つかった場合、まずi++で.をスキップします。- もし
afterIndexがtrueであれば(つまり、%.[2]dのようにインデックスの直後に.が続く場合)、これは不正なパターンとしてp.goodArgNumをfalseに設定します。 - 次に、精度の
*の前に引数インデックスが来る可能性を考慮して、再度p.argNumberを呼び出します。 *で精度が指定される場合、intFromArgを呼び出す前にi++で*をスキップし、afterIndexをfalseにリセットします。- 数値で精度が指定される場合 (
parsenum) は、通常の解析を行います。
- 最終的なインデックスチェック: 幅と精度の解析が完了した後、もし
afterIndexがまだfalseであれば、動詞の直前に引数インデックスが来る可能性を考慮して、もう一度p.argNumberを呼び出します。 - エラー処理:
p.goodArgNumがfalseに設定された場合、最終的にbadIndexBytesがバッファに書き込まれ、不正な書式指定がエラーとして報告されます。
これらの変更により、fmt パッケージは、書式指定文字列の解析において、引数インデックス、幅、精度の関係をドキュメントに記述された通りに厳密に評価するようになりました。これにより、%[3]*.[2]*[1]f のような複雑な書式指定も正しく解釈され、期待通りの出力が得られるようになります。
関連リンク
- Go言語
fmtパッケージのドキュメント: https://pkg.go.dev/fmt - Go言語の
printfフォーマット文字列に関する公式ブログ記事 (関連する概念の理解に役立つ可能性があります): https://go.dev/blog/go-fmt-printf
参考にした情報源リンク
- 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"