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

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

このコミットは、Go言語の標準ライブラリである fmt パッケージ内の sprintf, sprint, println 関数における修正を適用するものです。具体的には、これらの関数が可変引数(variadic arguments)を処理する方法が改善され、reflect パッケージを用いた引数の取り扱いがより正確に行われるようになっています。

コミット

commit 3a2c0a9615b3000f99d42647f4e811b960af755b
Author: Robert Griesemer <gri@golang.org>
Date:   Thu Nov 6 11:56:08 2008 -0800

    - fixes to sprintf (by rob)
    
    R=r
    OCL=18685
    CL=18685

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

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

元コミット内容

- fixes to sprintf (by rob)

変更の背景

このコミットが行われた2008年11月は、Go言語がまだ一般に公開される前の初期開発段階にありました。fmt パッケージは、C言語の printf ファミリー関数に似た書式設定されたI/Oを提供するGoの基本的なパッケージであり、その正確な動作は言語の使いやすさにとって非常に重要です。

初期のGo言語では、可変引数(variadic arguments)の扱いに関して、現在とは異なる実装やセマンティクスが存在していました。特に、sprintfsprint のような関数が可変引数を受け取る際に、それらの引数がどのように内部的に表現され、reflect パッケージを通じてアクセスされるかについて、初期の実装には不整合やバグがあったと考えられます。

このコミットの「fixes to sprintf」という簡潔なメッセージは、当時の sprintf 関数(および関連する sprint, println 関数)が、渡された引数を正しく解釈・処理できていなかった問題に対処するためのものであることを示唆しています。特に、可変引数が単一のインターフェース値として渡される際の reflect パッケージによるアンラップ処理に問題があった可能性が高いです。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と初期のGo言語の特性に関する知識が必要です。

  1. fmt パッケージ: Go言語の標準ライブラリの一つで、書式設定されたI/O(入出力)を提供します。Printf, Sprintf, Println などの関数が含まれ、C言語の printf に似た機能を提供しますが、Goの型システムとインターフェースの概念に基づいて設計されています。
  2. 可変引数(Variadic Functions): Go言語では、関数の最後のパラメータに ... を付けることで、任意の数の引数を受け取ることができます。これらの引数は関数内部ではスライスとして扱われます。例えば、func foo(args ...int) のように定義された関数では、args[]int 型のスライスになります。
  3. interface{}: Go言語における空のインターフェース型は、任意の型の値を保持できる「型安全なコンテナ」として機能します。fmt パッケージの関数は、しばしば interface{} 型の可変引数を受け取り、これにより様々な型の値を書式設定できます。
  4. reflect パッケージ: Go言語の標準ライブラリの一つで、実行時にプログラムの構造(型、値、メソッドなど)を検査・操作するための機能を提供します。リフレクションは、ジェネリックなプログラミングや、型がコンパイル時に不明な場合の処理(例: fmt パッケージでの任意の型の値の書式設定)に利用されます。
    • reflect.NewValue(i interface{}) Value: 任意のインターフェース値 i から reflect.Value を生成します。reflect.Value は、Goの実行時値の抽象表現です。
    • reflect.PtrValue: ポインタを表す reflect.Value の一種です。
    • reflect.PtrValue.Sub() Value: ポインタが指す先の値(要素)を表す reflect.Value を返します。
    • reflect.StructValue: 構造体を表す reflect.Value の一種です。
  5. Go言語の初期の可変引数と reflect の挙動: Go言語の初期バージョンでは、可変引数がどのように interface{} のスライスとして渡され、reflect パッケージでどのようにアンラップされるかについて、現在の安定版とは異なる挙動や、特定のケースでのバグが存在した可能性があります。特に、可変引数が単一の interface{} 値として渡された場合に、それがさらにポインタや構造体としてラップされている場合の処理が複雑でした。

技術的詳細

このコミットの核心は、sprintf, sprint, println 関数が可変引数 v ... を受け取る際の reflect パッケージの利用方法の変更にあります。

変更前は、これらの関数は可変引数 v ... を直接 reflect.NewValue(v).(reflect.StructValue) のように reflect.Value に変換しようとしていました。しかし、Go言語の可変引数は、実際には呼び出し側でスライスとして構築され、そのスライスが単一の interface{} 値として関数に渡されます。

問題は、この「スライスをラップした interface{}」が、さらにポインタとして扱われる場合があったことです。特に、... で渡された引数が一つもない場合や、単一の引数が渡された場合に、Goのコンパイラがどのようにそれを内部的に表現するかが、初期のバージョンでは現在のセマンティクスと異なっていた可能性があります。

このコミットでは、以下の変更が加えられています。

// 変更前:
// export func sprintf(format string, v ...) string {
//     p := Printer();
//     p.doprintf(format, reflect.NewValue(v).(reflect.StructValue));
//     s := string(p.buf)[0 : p.n];
//     return s;
// }

// 変更後:
export func sprintf(format string, a ...) string {
    v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
    p := Printer();
    p.doprintf(format, v);
    s := string(p.buf)[0 : p.n];
    return s;
}

変更後のコードでは、可変引数 a ... を受け取った後、まず reflect.NewValue(a)reflect.Value を取得します。そして、.(reflect.PtrValue) でそれがポインタであることをアサートし、.Sub() メソッドを呼び出してポインタが指す先の値(つまり、可変引数のスライスそのもの)を取得しています。最後に、. (reflect.StructValue) でその値が構造体(この場合は可変引数のスライスが内部的に構造体として扱われる)であることをアサートしています。

この一連の操作は、可変引数が interface{} のスライスとして渡される際に、そのスライスがさらにポインタとしてラップされているケースに正しく対応するためのものです。これにより、doprintfdoprint 関数に渡される v が、常に期待される reflect.StructValue(可変引数のスライスを表す)となるように保証されます。

この修正は、Go言語の fmt パッケージが、様々な型の可変引数をより堅牢かつ正確に処理できるようにするために不可欠でした。特に、リフレクションを多用する fmt のようなパッケージでは、実行時の型の表現と操作が正確であることが、予期せぬパニックや不正な出力を防ぐ上で極めて重要です。

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

src/lib/fmt/print.go ファイルにおいて、以下の3つの関数が変更されています。

  1. sprintf 関数
  2. sprint 関数
  3. println 関数

それぞれの関数で、可変引数 v ... の受け取り方が a ... に変更され、その areflect パッケージで処理するロジックが追加されています。

--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -28,11 +28,11 @@ export type Formatter interface {\
  	Precision()\t(prec int, ok bool);\
  }\
  
-export type Format interface {\
+type Format interface {\
  	Format(f Formatter, c int);\
  }\
  
-export type String interface {\
+type String interface {\
  	String() string
  }\
  
@@ -132,9 +132,10 @@ export func printf(format string, v ...) (n int, errno *os.Error) {\
  	return n, errno;\
  }\
  
-export func sprintf(format string, v ...) string {\
+export func sprintf(format string, a ...) string {\
+\tv := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);\
  \tp := Printer();\
-\tp.doprintf(format, reflect.NewValue(v).(reflect.StructValue));\
+\tp.doprintf(format, v);\
  \ts := string(p.buf)[0 : p.n];\
  \treturn s;\
  }\
@@ -155,9 +156,10 @@ export func print(v ...) (n int, errno *os.Error) {\
  	return n, errno;\
  }\
  
-export func sprint(v ...) string {\
+export func sprint(a ...) string {\
+\tv := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);\
  \tp := Printer();\
-\tp.doprint(reflect.NewValue(v).(reflect.StructValue), false, false);\
+\tp.doprint(v, false, false);\
  \ts := string(p.buf)[0 : p.n];\
  \treturn s;\
  }\
@@ -179,9 +181,10 @@ export func println(v ...) (n int, errno *os.Error) {\
  	return n, errno;\
  }\
  
-export func sprintln(v ...) string {\
+export func sprintln(a ...) string {\
+\tv := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);\
  \tp := Printer();\
-\tp.doprint(reflect.NewValue(v).(reflect.StructValue), true, true);\
+\tp.doprint(v, true, true);\
  \ts := string(p.buf)[0 : p.n];\
  \treturn s;\
  }\

また、export type Format interfaceexport type String interfaceexport キーワードが削除され、これらがパッケージ内部でのみ使用される型になったことも変更点として含まれています。これは、Go言語の初期の export キーワードのセマンティクスと、現在の public/private の概念(大文字で始まる識別子がエクスポートされる)への移行に関連する可能性があります。

コアとなるコードの解説

変更の核心は、sprintf, sprint, println の各関数内で、可変引数 areflect.Value に変換する以下の行です。

v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);

この行は、Go言語の可変引数がどのように内部的に処理されるか、そして reflect パッケージがそれをどのように「アンラップ」して実際の値にアクセスするかを示しています。

  1. reflect.NewValue(a): 関数に渡された可変引数 a(これは実際には interface{} 型のスライスとして扱われる)を reflect.Value 型に変換します。
  2. . (reflect.PtrValue): reflect.NewValue(a) の結果が reflect.PtrValue 型であることをアサートします。これは、Goの可変引数が内部的にポインタとしてラップされている可能性があることを示唆しています。
  3. .Sub(): reflect.PtrValueSub() メソッドを呼び出すことで、ポインタが指す先の値(つまり、可変引数のスライスそのもの)を取得します。この結果も reflect.Value 型です。
  4. . (reflect.StructValue): 最後に、.Sub() から返された reflect.Valuereflect.StructValue 型であることをアサートします。これは、Goの可変引数のスライスが、リフレクションの文脈では構造体として扱われることを意味します。

この一連の処理により、v は常に可変引数のスライスを正確に表現する reflect.StructValue となり、p.doprintfp.doprint といった内部関数が、渡された引数を正しく書式設定できるようになります。

export キーワードの削除については、Go言語の初期には export キーワードが識別子の可視性を明示するために使われていましたが、後に識別子の大文字・小文字によって可視性が決まる現在のルールに移行しました。この変更は、その移行期におけるコードベースの調整の一環であると考えられます。FormatString インターフェースがパッケージ外部に公開する必要がないと判断されたため、export が削除されたのでしょう。

関連リンク

参考にした情報源リンク