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

[インデックス 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 == niltrueを返します。
  • 空スライス:
    • make([]byte, 0)[]byte{}のように初期化されたスライスは空スライスです。
    • 長さも容量も0ですが、基底配列へのポインタはnilではありません(長さ0の有効な基底配列を指します)。
    • s == nilfalseを返します。

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であるかを明示的にチェックするロジックが追加されました。

具体的には、以下の条件が考慮されます:

  1. フォーマット対象がバイトスライスであること。
  2. goSyntaxフラグがtrueであること(%#vが指定されている)。
  3. バイトスライス自体がnilであること。

これらの条件が満たされた場合、fmtBytes関数は、スライスの型情報(reflect.Type)が利用可能かどうかに応じて、適切なGo構文文字列を生成します。型情報が利用できない場合は汎用的に"[]byte(nil)"と出力し、型情報が利用できる場合はその型名(例: []int32)と(nil)を結合して出力します。これにより、[]byte(nil)だけでなく、[]int32(nil)のような他の型のnilスライスも正しくフォーマットされるようになりました。

この変更は、fmtパッケージの出力がGo言語のセマンティクスにより忠実になることを保証し、特にリフレクションやコード生成を行うツールにとって、より信頼性の高いGo構文表現を提供します。

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

このコミットによる主要なコード変更は以下の2つのファイルにあります。

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

    • 新しいテストケースがfmtTests変数に追加されました。
    • {"%#v", []byte(nil), "[]byte(nil)"},
    • {"%#v", []int32(nil), "[]int32(nil)"},
  2. 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.gofmtBytes 関数の変更

fmtBytes関数は、fmtパッケージ内でバイトスライスを文字列に変換する主要なロジックを含んでいます。この関数への変更は、goSyntaxtrue(つまり%#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: この新しい条件分岐が追加され、フォーマット対象のバイトスライスvnilであるかどうかをチェックします。
  • if typ == nil: vnilであると判断された場合、さらにtypreflect.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トラッカーでは直接見つかりませんが、コミットメッセージに明記されています。)

参考にした情報源リンク