[インデックス 19025] ファイルの概要
このコミットは、Go言語の標準ライブラリであるfmt
パッケージにおいて、[]byte(nil)
のようなnil
スライスがGo構文 (%#v
) でフォーマットされる際に、その出力が正確なGoのシンタックスに準拠するように修正したものです。具体的には、[]byte(nil)
が[]
ではなく[]byte(nil)
と表示されるように改善されました。
コミット
commit c274ff6761ed8bdaea7d99fcaeb1116fff3763dd
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Thu Apr 3 16:11:03 2014 -0400
fmt: fix go syntax formatting of []byte(nil)
Fixes #7639.
LGTM=rsc
R=r, adg, rsc
CC=golang-codereviews
https://golang.org/cl/81240043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c274ff6761ed8bdaea7d99fcaeb1116fff3763dd
元コミット内容
fmt: fix go syntax formatting of []byte(nil)
Fixes #7639.
LGTM=rsc
R=r, adg, rsc
CC=golang-codereviews
https://golang.org/cl/81240043
変更の背景
Go言語のfmt
パッケージは、様々なGoの値を文字列としてフォーマットするための機能を提供します。特に、fmt.Printf
関数で使用されるフォーマット動詞%#v
は、値のGo構文表現を出力することを目的としています。しかし、このコミット以前は、[]byte(nil)
のようなnil
スライスが%#v
でフォーマットされた際に、期待されるGo構文である[]byte(nil)
ではなく、単に[]
と出力されてしまう問題がありました。
nil
スライスと空スライス([]byte{}
)は、どちらも長さと容量が0ですが、Goの内部表現では異なる概念です。nil
スライスは基底配列を持たないのに対し、空スライスは長さ0の基底配列を持ちます。%v
フォーマット動詞ではこれらを区別せず[]
と表示しますが、%#v
はGoのソースコードでその値を再構築できるような正確な表現を提供すべきです。
この不正確なGo構文表現は、デバッグ時やコード生成ツールなどで問題となる可能性がありました。このコミットは、このfmt
パッケージの挙動を修正し、nil
スライスが%#v
でフォーマットされた際に、その型情報を含んだ正確なGo構文(例: []byte(nil)
、[]int32(nil)
)で出力されるようにすることで、一貫性と正確性を向上させました。コミットメッセージに記載されているIssue #7639は、この問題が報告されたものと考えられますが、現在の公開Issueトラッカーでは直接参照できません。
前提知識の解説
Go言語のfmt
パッケージ
fmt
パッケージは、Go言語における基本的な入出力フォーマット機能を提供します。最もよく使われる関数の一つがfmt.Printf
で、これはC言語のprintf
に似た書式指定文字列を使って値をフォーマットします。
フォーマット動詞 %v
と %#v
%v
(Value): 値のデフォルト表現を出力します。ほとんどの型で適切に機能し、人間が読みやすい形式で表示されます。例えば、[]byte{1, 2, 3}
は[1 2 3]
と表示され、[]byte(nil)
は[]
と表示されます。%#v
(Go-syntax representation): 値のGo構文表現を出力します。これは、その値をGoのソースコードで再構築できるような形式を目指します。構造体やスライス、マップなどの複合型で特に有用です。例えば、構造体はフィールド名と値を含む形式で表示され、スライスは型情報を含む形式で表示されます。このコミットの文脈では、[]byte(nil)
が[]byte(nil)
と正確に表示されることが重要でした。
Goにおけるnil
スライス
Go言語において、スライスはデータ構造であり、基底配列へのポインタ、長さ、容量の3つの要素から構成されます。
nil
スライス:var s []byte
のように宣言された直後のスライスや、[]byte(nil)
と明示的にキャストされたスライスはnil
スライスです。- 基底配列へのポインタが
nil
であり、長さ(len(s)
)も容量(cap(s)
)も0です。 s == nil
はtrue
を返します。
- 空スライス:
make([]byte, 0)
や[]byte{}
のように初期化されたスライスは空スライスです。- 長さも容量も0ですが、基底配列へのポインタは
nil
ではありません(長さ0の有効な基底配列を指します)。 s == nil
はfalse
を返します。
nil
スライスと空スライスは、長さと容量が同じであるため、多くの操作(例: for range
ループ)では同じように振る舞いますが、nil
チェックやGo構文での表現においては明確に区別されます。この区別を%#v
で正確に表現することが、このコミットの目的でした。
技術的詳細
このコミットの技術的な核心は、fmt
パッケージがGoのnil
スライスをGo構文で正確に表現できるようにすることです。以前のfmt
パッケージのfmtBytes
関数は、%#v
フォーマット動詞が指定された場合でも、[]byte(nil)
を[]
と出力していました。これは、nil
スライスと空スライスを区別しない%v
の挙動と混同されがちですが、%#v
の目的である「Go構文での正確な表現」とは異なります。
修正は、src/pkg/fmt/print.go
ファイル内のfmtBytes
関数に集中しています。この関数は、バイトスライス([]byte
)のフォーマットを担当します。goSyntax
フラグがtrue
(つまり%#v
が使用されている)の場合に、渡されたバイトスライスがnil
であるかを明示的にチェックするロジックが追加されました。
具体的には、以下の条件が考慮されます:
- フォーマット対象がバイトスライスであること。
goSyntax
フラグがtrue
であること(%#v
が指定されている)。- バイトスライス自体が
nil
であること。
これらの条件が満たされた場合、fmtBytes
関数は、スライスの型情報(reflect.Type
)が利用可能かどうかに応じて、適切なGo構文文字列を生成します。型情報が利用できない場合は汎用的に"[]byte(nil)"
と出力し、型情報が利用できる場合はその型名(例: []int32
)と(nil)
を結合して出力します。これにより、[]byte(nil)
だけでなく、[]int32(nil)
のような他の型のnil
スライスも正しくフォーマットされるようになりました。
この変更は、fmt
パッケージの出力がGo言語のセマンティクスにより忠実になることを保証し、特にリフレクションやコード生成を行うツールにとって、より信頼性の高いGo構文表現を提供します。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の2つのファイルにあります。
-
src/pkg/fmt/fmt_test.go
:- 新しいテストケースが
fmtTests
変数に追加されました。 {"%#v", []byte(nil), "[]byte(nil)"},
{"%#v", []int32(nil), "[]int32(nil)"},
- 新しいテストケースが
-
src/pkg/fmt/print.go
:func (p *pp) fmtBytes(v []byte, verb rune, goSyntax bool, typ reflect.Type, depth int)
関数内に変更が加えられました。- 既存の
if goSyntax { ... }
ブロックの内部に、新しいif v == nil { ... }
ブロックが追加されました。
--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -403,6 +403,8 @@ var fmtTests = []struct {
{"%#v", "foo", `\"foo\"`},
{"%#v\", barray, `[5]fmt_test.renamedUint8{0x1, 0x2, 0x3, 0x4, 0x5}`},
{"%#v\", bslice, `[]fmt_test.renamedUint8{0x1, 0x2, 0x3, 0x4, 0x5}`},
+\t{\"%#v\", []byte(nil), \"[]byte(nil)\"},
+\t{\"%#v\", []int32(nil), \"[]int32(nil)\"},
// slices with other formats
{\"%#x\", []int{1, 2, 15}, `[0x1 0x2 0xf]`},
--- a/src/pkg/fmt/print.go
+++ b/src/pkg/fmt/print.go
@@ -523,6 +523,15 @@ func (p *pp) fmtString(v string, verb rune, goSyntax bool) {
func (p *pp) fmtBytes(v []byte, verb rune, goSyntax bool, typ reflect.Type, depth int) {
if verb == 'v' || verb == 'd' {
if goSyntax {
+\t\t\tif v == nil {
+\t\t\t\tif typ == nil {
+\t\t\t\t\tp.buf.WriteString(\"[]byte(nil)\")
+\t\t\t\t} else {
+\t\t\t\t\tp.buf.WriteString(typ.String())\n+\t\t\t\t\tp.buf.Write(nilParenBytes)\n+\t\t\t\t}\n+\t\t\t\treturn
+\t\t\t}
if typ == nil {
p.buf.Write(bytesBytes)
} else {
コアとなるコードの解説
src/pkg/fmt/fmt_test.go
の変更
追加されたテストケースは、このコミットの修正が正しく機能していることを検証するためのものです。
{"%#v", []byte(nil), "[]byte(nil)"}
: これは、nil
のバイトスライスを%#v
でフォーマットした場合に、期待される出力が"[]byte(nil)"
であることを確認します。{"%#v", []int32(nil), "[]int32(nil)"}
: 同様に、nil
の[]int32
スライスが"[]int32(nil)"
とフォーマットされることを確認します。これにより、この修正が[]byte
型だけでなく、他のスライス型にも汎用的に適用されることを示唆しています。
これらのテストケースは、修正が導入される前のfmt
パッケージでは失敗していたはずであり、修正が適用された後にパスすることで、問題が解決されたことを保証します。
src/pkg/fmt/print.go
の fmtBytes
関数の変更
fmtBytes
関数は、fmt
パッケージ内でバイトスライスを文字列に変換する主要なロジックを含んでいます。この関数への変更は、goSyntax
がtrue
(つまり%#v
フォーマット動詞が使用されている)の場合に、nil
スライスを特別に処理するようにします。
変更されたコードブロックは以下の通りです。
if goSyntax {
if v == nil { // ここでスライスがnilであるかをチェック
if typ == nil { // 型情報が利用できない場合 (例: interface{} として渡された場合)
p.buf.WriteString("[]byte(nil)") // 直接 "[]byte(nil)" を書き込む
} else { // 型情報が利用できる場合 (例: []int32(nil) のように具体的な型が分かっている場合)
p.buf.WriteString(typ.String()) // スライスの型名 (例: "[]int32") を書き込む
p.buf.Write(nilParenBytes) // その後に "(nil)" を書き込む
}
return // 処理を終了
}
// v が nil でない場合の既存のロジックが続く
if typ == nil {
p.buf.Write(bytesBytes)
} else {
// ...
}
}
if v == nil
: この新しい条件分岐が追加され、フォーマット対象のバイトスライスv
がnil
であるかどうかをチェックします。if typ == nil
:v
がnil
であると判断された場合、さらにtyp
(reflect.Type
型の情報)がnil
であるかをチェックします。typ == nil
の場合:これは、fmt
が型情報を取得できなかった場合(例えば、interface{}
として渡されたが、その動的な型がnil
である場合など)に発生する可能性があります。この場合、最も一般的なnil
バイトスライスのGo構文表現である"[]byte(nil)"
を直接出力します。typ != nil
の場合:fmt
はスライスの具体的な型情報を持っています(例:[]int32
)。この場合、typ.String()
を呼び出してその型名(例:"[]int32"
)を取得し、それをバッファに書き込みます。その後、nilParenBytes
(これは[]byte("(nil)")
を表す内部定数であると推測されます)を書き込むことで、"[]int32(nil)"
のような正確なGo構文表現を生成します。
return
:nil
スライスの処理が完了したら、それ以上フォーマットする必要がないため、関数を終了します。
この変更により、fmt
パッケージはnil
スライスを%#v
でフォーマットする際に、Go言語のセマンティクスに完全に準拠した正確なGo構文表現を出力するようになりました。
関連リンク
- Go CL (Change List): https://golang.org/cl/81240043 (このCLは、コミットメッセージに記載されていますが、現在のGoのコードレビューシステムでは別のコミットにリンクしています。これは、過去のシステムからの移行やCL番号の再利用によるものかもしれません。)
- Go issue #7639 (コミットが修正したとされるIssue): https://go.dev/issue/7639 (このIssueは現在の公開Issueトラッカーでは直接見つかりませんが、コミットメッセージに明記されています。)
参考にした情報源リンク
- Go言語公式ドキュメント:
fmt
パッケージ: https://pkg.go.dev/fmt - Go言語公式ドキュメント: スライス: https://go.dev/blog/slices-intro
- Go issue #10430:
fmt: empty byte slice prints one value with format "% 02x"
(fmt
パッケージの空スライスに関する関連議論): https://github.com/golang/go/issues/10430