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

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

コミット

commit 00134fe8ef917f17fc87076badbc54c086f74589
Author: Russ Cox <rsc@golang.org>
Date:   Tue Feb 7 23:37:05 2012 -0500

    fmt: diagnose invalid verb applied to pointer
    
    Fixes #2851.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5644048

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

https://github.com/golang/go/commit/00134fe8ef917f17fc87076badbc54c086f74589

元コミット内容

fmt: diagnose invalid verb applied to pointer

Fixes #2851.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5644048

変更の背景

このコミットは、Go言語の標準ライブラリであるfmtパッケージにおいて、ポインタ型に対して不適切な書式指定動詞(verb)が使用された場合に、適切な診断メッセージを出力するように改善することを目的としています。具体的には、Go issue #2851で報告された問題に対応しています。

Go issue #2851は、「fmt: accepts non strings/chars with verb %q」というタイトルで、ポインタ型のような文字列や文字ではない値に対して、文字列をクォートして出力するための%q動詞が適用された際に、予期せぬ出力やエラーが発生しないという問題点を指摘していました。本来、%qは文字列やバイトスライスに適用されるべきであり、ポインタのような型に適用された場合は、開発者にその誤用を知らせるべきです。

この変更以前は、fmtパッケージはポインタに対して不適切な動詞が使用されても、エラーを報告せずにデフォルトの書式で出力しようとするか、あるいは予期せぬ結果を招く可能性がありました。このコミットは、このような誤用を早期に検出し、開発者に明確なフィードバックを提供することで、デバッグの労力を削減し、より堅牢なコードの記述を促進します。

前提知識の解説

Go言語のfmtパッケージ

fmtパッケージは、Go言語における書式付きI/O(入出力)を実装するためのパッケージです。C言語のprintfscanfに似た機能を提供し、様々なデータ型を整形して文字列として出力したり、文字列からデータを読み取ったりすることができます。

主な関数には以下のようなものがあります。

  • fmt.Print, fmt.Println, fmt.Printf: 標準出力への出力
  • fmt.Sprint, fmt.Sprintln, fmt.Sprintf: 文字列への出力
  • fmt.Fprint, fmt.Fprintln, fmt.Fprintf: 指定されたio.Writerへの出力

書式指定動詞(Verbs)

fmt.Printfなどの書式付き関数では、出力する値の型や表示形式を指定するために「書式指定動詞(verb)」と呼ばれるプレースホルダーを使用します。動詞は%で始まり、その後に文字が続きます。

本コミットに関連する主な動詞は以下の通りです。

  • %p: ポインタのアドレスを16進数で表示します。0xプレフィックスが付きます。
  • %v: 値のデフォルトの書式で表示します。構造体や配列など、様々な型に対応しています。
  • %q: Goの構文でクォートされた文字列リテラルとして表示します。非表示文字はエスケープされます。主に文字列型に適用されます。
  • %s: 文字列またはバイトスライスをそのまま表示します。
  • %d: 整数値を10進数で表示します。
  • %x, %X: 整数値を16進数で表示します(%xは小文字、%Xは大文字)。
  • %b: 整数値を2進数で表示します。
  • %o: 整数値を8進数で表示します。

reflectパッケージ

reflectパッケージは、Goプログラムの実行時に、変数や関数の型情報を動的に検査したり、値を操作したりするための機能を提供します。リフレクションは、ジェネリックなコードや、型が事前にわからないデータを扱う場合に特に有用です。

  • reflect.Value: Goのあらゆる値のランタイム表現です。reflect.ValueOf(i interface{})関数を使って、任意のインターフェース値からreflect.Valueを取得できます。
  • reflect.Kind: reflect.Valueが表す値の基本的な種類(例: Int, String, Struct, Ptr, Chan, Func, Map, Slice, UnsafePointerなど)を返します。
  • reflect.Type: Goの型を表します。reflect.TypeOf(i interface{})関数を使って、任意のインターフェース値からreflect.Typeを取得できます。

fmtパッケージは、内部でreflectパッケージを使用して、引数として渡された値の型情報を動的に取得し、それに基づいて適切な書式設定ロジックを選択します。

Stringerインターフェース

fmtパッケージは、Stringerインターフェースを実装する型に対して特別な扱いをします。Stringerインターフェースは以下のように定義されています。

type Stringer interface {
    String() string
}

ある型がString() stringメソッドを実装している場合、fmtパッケージはその型の値を%v%sなどの動詞で出力する際に、そのString()メソッドの戻り値を使用します。これにより、カスタムの文字列表現を提供できます。

技術的詳細

このコミットの主要な変更は、src/pkg/fmt/print.goファイル内のpp構造体のfmtPointerメソッドにあります。fmtPointerメソッドは、reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointerといったポインタに関連する型を整形する役割を担っています。

変更前は、fmtPointerメソッドは渡されたverb(書式指定動詞)がポインタに対して適切かどうかを明示的にチェックしていませんでした。そのため、例えばポインタに対して%qのような不適切な動詞が指定された場合でも、エラーを報告せずに処理を続行しようとしていました。これは、開発者が意図しない出力を受け取ったり、デバッグが困難になったりする原因となっていました。

このコミットでは、fmtPointerメソッドの冒頭にswitch文が追加されました。このswitch文は、verb'p', 'v', 'b', 'd', 'o', 'x', 'X'のいずれかであるかをチェックします。これらの動詞はポインタに対して意味のある書式設定を提供するため、「ok」と判断されます。もしverbがこれらの許可された動詞のいずれでもない場合、p.badVerb(verb)が呼び出され、不適切な動詞が使用されたことを示すエラーメッセージが生成されます。これにより、コンパイル時ではなく実行時に、ポインタに対する書式指定の誤りを診断できるようになります。

また、このコミットでは、%v動詞でnilポインタが整形される際の挙動も改善されています。変更前は、nilポインタを%vで出力すると、0x0のようなアドレス値が表示されることがありました。変更後は、%vnilポインタを整形した場合に、<nil>という文字列が出力されるようになります。これは、Go言語の他の部分でのnilの表現と一貫性を持たせ、より直感的な出力にするための変更です。

さらに、src/pkg/fmt/fmt_test.goには、これらの変更を検証するための新しいテストケースが追加されています。

  • {"p3=%p", (*int)(nil), "p3=0x0"}: nilポインタを%pで整形した場合の出力が0x0になることを確認。
  • {"%q", (*int)(nil), "%!q(*int=<nil>)"}: nilポインタを%qで整形した場合に、%!q(*int=<nil>)というエラー診断メッセージが出力されることを確認。これは、%qがポインタに対して不適切な動詞であることを示しています。
  • {"%q", new(int), "%!q(*int=0xPTR)"}: new(int)で作成されたポインタを%qで整形した場合に、同様のエラー診断メッセージが出力されることを確認。
  • {"%v", (*int)(nil), "<nil>"}: nilポインタを%vで整形した場合に、<nil>が出力されることを確認。
  • {"%v", new(int), "0xPTR"}: new(int)で作成されたポインタを%vで整形した場合に、アドレス値が出力されることを確認。

これらのテストケースは、fmtPointerメソッドの新しい診断ロジックと、%vによるnilポインタの整形挙動が期待通りに機能することを保証します。

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

src/pkg/fmt/fmt_test.go

--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -423,6 +423,7 @@ var fmttests = []struct {
 	{"p0=%p", new(int), "p0=0xPTR"},
 	{"p1=%s", &pValue, "p1=String(p)"}, // String method...
 	{"p2=%p", &pValue, "p2=0xPTR"},     // ... not called with %p
+	{"p3=%p", (*int)(nil), "p3=0x0"},
 	{"p4=%#p", new(int), "p4=PTR"},
 
 	// %p on non-pointers
@@ -431,6 +432,14 @@ var fmttests = []struct {
 	{"%p", make([]int, 1), "0xPTR"},
 	{"%p", 27, "%!p(int=27)"}, // not a pointer at all
 
+	// %q on pointers
+	{"%q", (*int)(nil), "%!q(*int=<nil>)"},
+	{"%q", new(int), "%!q(*int=0xPTR)"},
+
+	// %v on pointers formats 0 as <nil>
+	{"%v", (*int)(nil), "<nil>"},
+	{"%v", new(int), "0xPTR"},
+
 	// %d on Stringer should give integer if possible
 	{"%s", time.Time{}.Month(), "January"},
 	{"%d", time.Time{}.Month(), "1"},

src/pkg/fmt/print.go

--- a/src/pkg/fmt/print.go
+++ b/src/pkg/fmt/print.go
@@ -553,6 +553,14 @@ func (p *pp) fmtBytes(v []byte, verb rune, goSyntax bool, depth int) {
 }
 
 func (p *pp) fmtPointer(value reflect.Value, verb rune, goSyntax bool) {
+	switch verb {
+	case 'p', 'v', 'b', 'd', 'o', 'x', 'X':
+		// ok
+	default:
+		p.badVerb(verb)
+		return
+	}
+
 	var u uintptr
 	switch value.Kind() {
 	case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
@@ -561,6 +569,7 @@ func (p *pp) fmtPointer(value reflect.Value, verb rune, goSyntax bool) {
 		p.badVerb(verb)
 		return
 	}
+
 	if goSyntax {
 		p.add('(')
 		p.buf.WriteString(value.Type().String())
@@ -572,6 +581,8 @@ func (p *pp) fmt0x64(uint64(u), true)
 		p.fmt0x64(uint64(u), true)
 		}
 		p.add(')')
+	} else if verb == 'v' && u == 0 {
+		p.buf.Write(nilAngleBytes)
 	} else {
 		p.fmt0x64(uint64(u), !p.fmt.sharp)
 	}
@@ -929,24 +940,7 @@ BigSwitch:
 			break BigSwitch
 		}
 	}
-	if goSyntax {
-		p.buf.WriteByte('(')
-		p.buf.WriteString(value.Type().String())
-		p.buf.WriteByte(')')
-		p.buf.WriteByte('(')
-		if v == 0 {
-			p.buf.Write(nilBytes)
-		} else {
-			p.fmt0x64(uint64(v), true)
-		}
-		p.buf.WriteByte(')')
-		break
-	}
-	if v == 0 {
-		p.buf.Write(nilAngleBytes)
-		break
-	}
-	p.fmt0x64(uint64(v), true)
+	fallthrough
 	case reflect.Chan, reflect.Func, reflect.UnsafePointer:
 	p.fmtPointer(value, verb, goSyntax)
 	default:

コアとなるコードの解説

src/pkg/fmt/fmt_test.goの変更

このファイルでは、fmttestsというテストケースのスライスに新しいエントリが追加されています。

  • {"p3=%p", (*int)(nil), "p3=0x0"}:
    • (*int)(nil)nil*int型ポインタを表します。
    • %p動詞で整形した場合、その出力が0x0となることを期待しています。これは、nilポインタのアドレスが0であることを示します。
  • // %q on pointers以下の2行:
    • {"%q", (*int)(nil), "%!q(*int=<nil>)"}: nil*int型ポインタを%qで整形しようとした場合、fmtパッケージが「%q*int型には不適切であり、値は<nil>である」という診断メッセージを出力することを期待しています。
    • {"%q", new(int), "%!q(*int=0xPTR)"}: new(int)で作成された*int型ポインタ(非nil)を%qで整形しようとした場合、同様に「%q*int型には不適切であり、値は0xPTR(実際のアドレス)である」という診断メッセージが出力されることを期待しています。0xPTRはテストフレームワークが実際のアドレス値に置き換えるプレースホルダーです。
  • // %v on pointers formats 0 as <nil>以下の2行:
    • {"%v", (*int)(nil), "<nil>"}: nil*int型ポインタを%vで整形した場合、その出力が<nil>となることを期待しています。これは、nilポインタのより直感的な表現です。
    • {"%v", new(int), "0xPTR"}: new(int)で作成された*int型ポインタを%vで整形した場合、その出力が0xPTR(実際のアドレス)となることを期待しています。非nilポインタはアドレス値として表示されます。

これらのテストケースは、fmtパッケージがポインタに対する不適切な動詞を正しく診断し、また%v動詞でnilポインタを適切に整形できるようになったことを検証します。

src/pkg/fmt/print.goの変更

このファイルでは、pp構造体のfmtPointerメソッドが変更されています。

  • func (p *pp) fmtPointer(value reflect.Value, verb rune, goSyntax bool)の冒頭に追加されたswitch:

    switch verb {
    case 'p', 'v', 'b', 'd', 'o', 'x', 'X':
        // ok
    default:
        p.badVerb(verb)
        return
    }
    

    このswitch文は、fmtPointerメソッドが処理するポインタ型に対して、どの書式指定動詞が許可されているかを明示的に定義しています。

    • 'p' (ポインタアドレス), 'v' (デフォルト), 'b' (2進数), 'd' (10進数), 'o' (8進数), 'x', 'X' (16進数) はポインタに対して有効な動詞と見なされます。
    • これらのいずれでもない動詞(例: 'q', 's'など)が指定された場合、p.badVerb(verb)が呼び出されます。badVerbメソッドは、%!verb(type=value)のような形式のエラーメッセージを生成し、出力バッファに追加します。これにより、開発者は不適切な動詞の使用を即座に認識できます。
    • return文により、不適切な動詞が検出された場合はそれ以上の処理を行わずにメソッドを終了します。
  • else if verb == 'v' && u == 0 { p.buf.Write(nilAngleBytes) }の追加:

    } else if verb == 'v' && u == 0 {
        p.buf.Write(nilAngleBytes)
    } else {
        p.fmt0x64(uint64(u), !p.fmt.sharp)
    }
    

    この変更は、%v動詞でポインタを整形する際の挙動を改善します。

    • uはポインタのアドレスを表すuintptr型の変数です。
    • u == 0は、ポインタがnilであることを意味します。
    • verb == 'v'かつu == 0の場合、p.buf.Write(nilAngleBytes)が実行されます。nilAngleBytes<nil>というバイトスライスを保持しており、これによりnilポインタが<nil>と出力されるようになります。
    • この変更により、nilポインタの%v出力がより明確で一貫性のあるものになります。
  • BigSwitchラベル内のポインタ整形ロジックの削除とfallthroughの追加:

    -	if goSyntax {
    -		p.buf.WriteByte('(')
    -		p.buf.WriteString(value.Type().String())
    -		p.buf.WriteByte(')')
    -		p.buf.WriteByte('(')
    -		if v == 0 {
    -			p.buf.Write(nilBytes)
    -		} else {
    -			p.fmt0x64(uint64(v), true)
    -		}
    -		p.buf.WriteByte(')')
    -		break
    -	}
    -	if v == 0 {
    -		p.buf.Write(nilAngleBytes)
    -		break
    -	}
    -	p.fmt0x64(uint64(v), true)
    +	fallthrough
    	case reflect.Chan, reflect.Func, reflect.UnsafePointer:
    	p.fmtPointer(value, verb, goSyntax)
    

    この部分の変更は、reflect.Ptr(一般的なポインタ)のケースから、reflect.Chan, reflect.Func, reflect.UnsafePointerのケースへの処理の流れを変更しています。

    • 以前はreflect.Ptrのケースで独自のポインタ整形ロジックを持っていましたが、これが削除されました。
    • 代わりにfallthroughキーワードが追加され、reflect.Ptrのケースの処理が、その次のcase reflect.Chan, reflect.Func, reflect.UnsafePointer:のブロックに引き継がれるようになりました。
    • このブロックでは、p.fmtPointer(value, verb, goSyntax)が呼び出されます。これにより、すべてのポインタ関連の型(reflect.Ptrを含む)の整形が、新しく診断ロジックが追加されたfmtPointerメソッドに一元化されることになります。これはコードの重複を排除し、保守性を向上させるためのリファクタリングです。

これらの変更により、fmtパッケージはポインタに対する書式指定の誤用をより効果的に検出し、開発者により有用なフィードバックを提供できるようになりました。また、nilポインタの%v出力も改善され、全体的な使いやすさが向上しています。

関連リンク

参考にした情報源リンク