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

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

このコミットは、Go言語の標準ライブラリであるfmtパッケージにおけるポインタの書式設定の挙動を変更するものです。具体的には、ポインタを%d%o%x%Xといった整数基数指定の書式動詞で出力する際に、従来のデフォルトであった0xプレフィックスを抑制し、純粋な数値として扱えるようにする改善が加えられています。これにより、開発者はポインタの値をより柔軟な形式で表示できるようになります。

コミット

commit 3ba0f6daf534721e4e671d00c9d3144296eb9a1a
Author: Rob Pike <r@golang.org>
Date:   Fri Aug 17 16:12:25 2012 -0700

    fmt: honor integer radix formats (%d etc.) for pointers
    Before, pointers always appeared as 0x1234ABCD. This CL
    keeps that as the default for %p and %v, but lets explicit
    numeric verbs override the default.
    Fixes #3936.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/6441152

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

https://github.com/golang/go/commit/3ba0f6daf534721e4e671d00c9d3144296eb9a1a

元コミット内容

fmt: honor integer radix formats (%d etc.) for pointers Before, pointers always appeared as 0x1234ABCD. This CL keeps that as the default for %p and %v, but lets explicit numeric verbs override the default. Fixes #3936.

変更の背景

Go言語のfmtパッケージは、様々なデータ型を文字列にフォーマットするための強力な機能を提供します。しかし、このコミットが適用される以前は、ポインタの書式設定には一貫性のない挙動がありました。具体的には、ポインタは常に%p%vといった書式動詞を使用した場合と同様に、0xプレフィックスが付加された16進数形式(例: 0x1234ABCD)で出力されていました。これは、ポインタのメモリアドレスを純粋な数値として、例えば10進数や8進数、あるいはプレフィックスなしの16進数として表示したい場合に不便でした。

この制限は、Issue #3936として報告されており、開発者からの要望があったことが示唆されます。ポインタの値を他の数値型と同様に、指定された基数(10進数、8進数、16進数)で柔軟にフォーマットできるようにすることで、fmtパッケージの使い勝手と表現力が向上します。このコミットは、この問題を解決し、%d%o%x%Xなどの整数基数指定の書式動詞がポインタに対しても期待通りに機能するようにするためのものです。

前提知識の解説

このコミットの理解を深めるために、以下のGo言語の概念とfmtパッケージの機能について解説します。

Go言語のfmtパッケージ

fmtパッケージは、Go言語におけるI/Oフォーマット機能を提供します。C言語のprintfscanfに似た関数群を持ち、様々なデータ型を整形して出力したり、文字列から値を読み取ったりすることができます。主な関数にはfmt.Printf(標準出力への整形出力)、fmt.Sprintf(文字列への整形出力)、fmt.Fprint(指定されたio.Writerへの整形出力)などがあります。

書式動詞 (Format Verbs)

fmtパッケージでは、値をどのように整形するかを指示するために「書式動詞」と呼ばれる特殊な文字を使用します。

  • %p: ポインタの値を16進数で表示します。通常、0xプレフィックスが付きます。
  • %v: 値をデフォルトの書式で表示します。ポインタの場合、%pと同様の形式になります。
  • %d: 10進数で表示します。
  • %o: 8進数で表示します。
  • %x: 16進数で表示します(小文字)。
  • %X: 16進数で表示します(大文字)。
  • %b: 2進数で表示します。

ポインタ (Pointers)

Go言語におけるポインタは、変数のメモリアドレスを格納する変数です。C言語のポインタと同様に、間接参照(デリファレンス)によってそのアドレスに格納されている値にアクセスできます。ポインタは、関数間で大きな構造体をコピーせずに渡したり、データ構造を動的に構築したりする際に使用されます。

基数 (Radix)

数値の表現形式における「基数」とは、その数値を構成する桁が取りうる値の数を指します。例えば、10進数は基数10(0-9)、8進数は基数8(0-7)、16進数は基数16(0-9, A-F)です。

reflectパッケージとreflect.Value

reflectパッケージは、Goプログラムが実行時に自身の構造を検査(リフレクション)することを可能にします。reflect.Valueは、Goの任意の型の値を抽象的に表現する型です。fmtパッケージのような汎用的なフォーマット関数は、reflectパッケージを使用して、与えられた引数の型や値を実行時に動的に調べ、適切な書式設定を適用します。fmtPointer関数は、reflect.Valueを使用してポインタの値を抽出し、そのアドレスを数値として扱います。

技術的詳細

このコミットの技術的な核心は、src/pkg/fmt/print.go内のfmtPointer関数の変更にあります。この関数は、fmtパッケージがポインタの値を文字列に変換する際の主要なロジックを担っています。

fmtPointer関数の変更点

  1. use0x64フラグの導入: fmtPointer関数内にuse0x64というブーリアン型の変数が導入され、初期値はtrueに設定されます。このフラグは、ポインタの書式設定において0xプレフィックスを使用するかどうかを制御します。

  2. 書式動詞による条件分岐の強化: switch verb文が変更され、書式動詞に基づいてuse0x64フラグの値が動的に設定されるようになりました。

    • case 'p', 'v': これらの書式動詞(ポインタのデフォルト表示、汎用表示)の場合、use0x64trueのままです。これは、%p%vが引き続き0xプレフィックス付きの16進数形式でポインタを表示することを意味します。
    • case 'b', 'd', 'o', 'x', 'X': これらの書式動詞(2進数、10進数、8進数、16進数)の場合、use0x64falseに設定されます。これがこのコミットの主要な変更点であり、これらの書式動詞が使用された際には0xプレフィックスが抑制されるようになります。
  3. 実際の書式設定ロジックの分岐: ポインタの実際の数値がuとして取得された後、その書式設定を行う部分に条件分岐が追加されました。

    • if use0x64: use0x64trueの場合、p.fmt0x64(uint64(u), !p.fmt.sharp)が呼び出されます。fmt0x64関数は、0xプレフィックスを付加して16進数形式で数値を整形する役割を担っています。
    • else: use0x64falseの場合、p.fmtUint64(uint64(u), verb, false)が呼び出されます。fmtUint64関数は、指定された書式動詞(verb)に従って符号なし整数を整形しますが、0xプレフィックスは付加しません。これにより、ポインタの値が純粋な数値として、指定された基数で出力されるようになります。

テストコードの変更点 (src/pkg/fmt/fmt_test.go)

この変更の正しさを検証するために、テストコードも更新されています。

  1. 新しいテストケースの追加: fmttests配列に、ポインタを整数基数でフォーマットする新しいテストケースが追加されました。

    • {"%d", new(int), "PTR_d"}: ポインタを10進数で表示。
    • {"%o", new(int), "PTR_o"}: ポインタを8進数で表示。
    • {"%x", new(int), "PTR_x"}: ポインタを16進数(小文字)で表示。 これらのテストケースでは、期待される出力文字列にPTR_dPTR_oPTR_xといったプレースホルダーが使用されており、テスト実行時に実際のポインタアドレスの数値部分に置き換えられます。
  2. TestSprintf関数のPTR置換ロジックの改善: TestSprintf関数内で、ポインタのアドレスをテストの比較対象から除外するために、出力文字列中のポインタ部分をPTRという文字列に置換するロジックが強化されました。

    • patternchars変数が導入され、PTR_dPTR_oPTR_xといった新しいプレースホルダーに対応できるようになりました。
    • chars変数は、それぞれの基数(10進数なら0-9、8進数なら0-7、16進数なら0-9a-fA-F)に対応する有効な文字セットを定義します。
    • strings.ContainsRune(chars, rune(c))を使用することで、出力されたポインタアドレスの各文字が、その基数で有効な文字であるかをより汎用的にチェックできるようになりました。これにより、テストがより堅牢になり、新しい書式設定の挙動を正確に検証できるようになっています。

これらの変更により、fmtパッケージはポインタの書式設定においてより柔軟な制御を提供し、開発者の期待に応える挙動を実現しています。

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

src/pkg/fmt/print.go

--- a/src/pkg/fmt/print.go
+++ b/src/pkg/fmt/print.go
@@ -585,8 +585,12 @@ func (p *pp) fmtBytes(v []byte, verb rune, goSyntax bool, depth int) {
 }
 
 func (p *pp) fmtPointer(value reflect.Value, verb rune, goSyntax bool) {
+	use0x64 := true
 	switch verb {
-\tcase 'p', 'v', 'b', 'd', 'o', 'x', 'X':
+\tcase 'p', 'v':
+\t	// ok
+\tcase 'b', 'd', 'o', 'x', 'X':
+\t	use0x64 = false
+\t	// ok
 	default:
 	\tp.badVerb(verb)
 	}
@@ -616,7 +620,11 @@ func (p *pp) fmtPointer(value reflect.Value, verb rune, goSyntax bool) {
 	} else if verb == 'v' && u == 0 {
 	\tp.buf.Write(nilAngleBytes)
 	} else {
-\t\tp.fmt0x64(uint64(u), !p.fmt.sharp)
+\t\tif use0x64 {
+\t\t\tp.fmt0x64(uint64(u), !p.fmt.sharp)
+\t\t} else {
+\t\t\tp.fmtUint64(uint64(u), verb, false)
+\t\t}
 	}
 }
 

src/pkg/fmt/fmt_test.go

--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -442,6 +442,11 @@ var fmttests = []struct {
 	{"%v", (*int)(nil), "<nil>"},
 	{"%v", new(int), "0xPTR"},
 
+	// %d etc. pointers use specified base.
+	{"%d", new(int), "PTR_d"},
+	{"%o", new(int), "PTR_o"},
+	{"%x", new(int), "PTR_x"},
+
 	// %d on Stringer should give integer if possible
 	{"%s", time.Time{}.Month(), "January"},
 	{"%d", time.Time{}.Month(), "1"},
@@ -471,14 +476,26 @@ func TestSprintf(t *testing.T) {
 	for _, tt := range fmttests {
 		s := Sprintf(tt.fmt, tt.val)
 		if i := strings.Index(tt.out, "PTR"); i >= 0 {
+			pattern := "PTR"
+			chars := "0123456789abcdefABCDEF"
+			switch {
+			case strings.HasPrefix(tt.out[i:], "PTR_d"):
+				pattern = "PTR_d"
+				chars = chars[:10]
+			case strings.HasPrefix(tt.out[i:], "PTR_o"):
+				pattern = "PTR_o"
+				chars = chars[:8]
+			case strings.HasPrefix(tt.out[i:], "PTR_x"):
+				pattern = "PTR_x"
+			}
 			j := i
 			for ; j < len(s); j++ {
 				c := s[j]
-\t\t\t\tif (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
+\t\t\t\tif !strings.ContainsRune(chars, rune(c)) {
 					break
 				}
 			}
-\t\t\ts = s[0:i] + "PTR" + s[j:]
+\t\t\ts = s[0:i] + pattern + s[j:]
 		}
 		if s != tt.out {
 			if _, ok := tt.val.(string); ok {

コアとなるコードの解説

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

fmtPointer関数は、reflect.Valueとして渡されたポインタの値を処理し、指定された書式動詞(verb)に従って文字列に変換します。

  1. use0x64 := true: この行で、use0x64という新しいブーリアン変数が導入され、デフォルトでtrueに初期化されます。これは、ポインタのデフォルトの書式設定(%p%v)が引き続き0xプレフィックス付きの16進数形式であることを示しています。

  2. switch verb文の変更:

    • 元のコードでは、'p', 'v', 'b', 'd', 'o', 'x', 'X'のすべての書式動詞に対して同じ処理(コメントアウトされた行)が行われていました。
    • 変更後、'p''v'のケースは独立し、use0x64trueのままです。
    • 新たに'b', 'd', 'o', 'x', 'X'のケースが追加され、これらの書式動詞が使用された場合にuse0x64 = falseが設定されます。これにより、これらの整数基数指定の書式動詞では0xプレフィックスが抑制されるようになります。
  3. 書式設定ロジックの条件分岐: ポインタの数値uが取得された後、実際の文字列変換ロジックが変更されました。

    • if use0x64: use0x64trueの場合(つまり、%pまたは%vが使用された場合)、p.fmt0x64(uint64(u), !p.fmt.sharp)が呼び出されます。この関数は、0xプレフィックスを付加した16進数形式で数値を整形します。
    • else: use0x64falseの場合(つまり、%b, %d, %o, %x, %Xのいずれかが使用された場合)、p.fmtUint64(uint64(u), verb, false)が呼び出されます。この関数は、指定されたverb(基数)に従って符号なし整数を整形しますが、0xプレフィックスは付加しません。これにより、ポインタのメモリアドレスが純粋な数値として、指定された基数で出力されるようになります。

この変更により、fmtパッケージはポインタの書式設定において、より細かく、かつ直感的な制御を提供できるようになりました。

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

テストファイルでは、新しいポインタの書式設定の挙動を検証するためのテストケースが追加され、既存のテストヘルパーロジックが改善されました。

  1. fmttests配列への追加: fmttestsは、fmt.Sprintfのテストケースを定義する構造体のスライスです。

    • {"%d", new(int), "PTR_d"}
    • {"%o", new(int), "PTR_o"}
    • {"%x", new(int), "PTR_x"} これらの行は、new(int)(任意のポインタを生成)をそれぞれの書式動詞でフォーマットした際の期待される出力パターンを定義しています。PTR_dPTR_oPTR_xは、テスト実行時に実際のポインタアドレスの数値部分に置き換えられるプレースホルダーです。
  2. TestSprintf関数のPTR置換ロジックの改善: TestSprintf関数内には、ポインタのアドレスが実行環境によって異なるため、テストの比較を容易にするために実際のポインタアドレスを汎用的な文字列(PTR)に置換するロジックがあります。このコミットでは、そのロジックが拡張されました。

    • patternchars変数が導入されました。patternは置換後のプレースホルダー(PTR, PTR_d, PTR_o, PTR_x)を、charsはそのプレースホルダーに対応する数値の有効文字セットを定義します。
    • switch文が追加され、tt.out(期待される出力)がPTR_dPTR_oPTR_xのいずれかで始まるかによって、patterncharsが適切に設定されます。例えば、PTR_dの場合はcharsが10進数文字(0-9)のみに制限されます。
    • if !strings.ContainsRune(chars, rune(c))という条件が導入されました。これは、出力されたポインタアドレスの各文字cが、その基数で有効な文字セットcharsに含まれているかをチェックします。これにより、テストはポインタアドレスの数値部分が正しい基数で出力されていることを検証できます。
    • 最後に、s = s[0:i] + pattern + s[j:]によって、実際のポインタアドレスの文字列が、対応するpatternPTR, PTR_dなど)に置き換えられ、期待される出力と比較されます。

これらのテストの変更は、fmtPointer関数の新しい挙動が正しく実装されていることを確認するための重要な役割を果たしています。

関連リンク

参考にした情報源リンク