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

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

このコミットは、Go言語の標準ライブラリである fmt パッケージ内のプリンタ(P struct)の内部実装を大幅に改修し、再帰的なprintf呼び出しを安全に処理できるように準備するものです。具体的には、P structをWriterインターフェースに準拠させ、fmtパッケージの主要な出力関数(fprintf, printf, sprintfなど)が呼び出されるたびに新しいPインスタンスを生成するように変更しています。これにより、カスタムフォーマット動詞(custom verbs)内でfmt関数を再帰的に使用する際の競合や状態破壊を防ぎます。

コミット

commit 3200b06b14ba24551921cfa76da94c374d8f3e8d
Author: Rob Pike <r@golang.org>
Date:   Tue Nov 4 13:57:21 2008 -0800

    prepare for recursive printfs
    
    R=rsc
    DELTA=31  (9 added, 6 deleted, 16 changed)
    OCL=18470
    CL=18472

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

https://github.com/golang/go/commit/3200b06b14ba24551921cfa76da94c374d8f3e8d

元コミット内容

--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -18,13 +18,13 @@ import (
 const Runeself = 0x80
 const AllocSize = 32
 
-export type P struct {
+type P struct {
 	n	int;
 	buf	*[]byte;
 	fmt	*Fmt;
 }
 
-export func Printer() *P {
+func Printer() *P {
 	p := new(P);
 	p.fmt = fmt.New();
 	return p;
@@ -74,8 +74,11 @@ func (p *P) add(c int) {
 	}
 }
 
-func (p *P) reset() {
-	p.n = 0;
+// Implement Write so we can call fprintf on a P, for
+// recursive use in custom verbs.
+func (p *P) Write(b *[]byte) (ret int, err *os.Error) {
+	p.addbytes(b, 0, len(b));
+	return len(b), nil;
 }
 
 export type Writer interface {
@@ -87,46 +90,46 @@ func (p *P) doprint(v reflect.StructValue, addspace, addnewline bool);
 
 // These routines end in 'f' and take a format string.
 
-func (p *P) fprintf(w Writer, format string, a ...) (n int, error *os.Error) {
+export func fprintf(w Writer, format string, a ...) (n int, error *os.Error) {
 	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
+\tp := Printer();
 	p.doprintf(format, v);
 	n, error = w.Write(p.buf[0:p.n]);
-\tp.reset();
 	return n, error;
 }
 
-func (p *P) printf(format string, v ...) (n int, errno *os.Error) {
-	n, errno = p.fprintf(os.Stdout, format, v);
+export func printf(format string, v ...) (n int, errno *os.Error) {
+\tn, errno = fprintf(os.Stdout, format, v);
 	return n, errno;
 }
 
-func (p *P) sprintf(format string, v ...) string {
+export func sprintf(format string, v ...) string {
+\tp := Printer();
 	p.doprintf(format, reflect.NewValue(v).(reflect.StructValue));
 	s := string(p.buf)[0 : p.n];
-\tp.reset();
 	return s;
 }
 
 // These routines do not take a format string and add spaces only
 // when the operand on neither side is a string.
 
-func (p *P) fprint(w Writer, a ...) (n int, error *os.Error) {
+export func fprint(w Writer, a ...) (n int, error *os.Error) {
 	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
+\tp := Printer();
 	p.doprint(v, false, false);
 	n, error = w.Write(p.buf[0:p.n]);
-\tp.reset();
 	return n, error;
 }
 
-func (p *P) print(v ...) (n int, errno *os.Error) {
-	n, errno = p.fprint(os.Stdout, v);
+export func print(v ...) (n int, errno *os.Error) {
+\tn, errno = fprint(os.Stdout, v);
 	return n, errno;
 }
 
-func (p *P) sprint(v ...) string {
+export func sprint(v ...) string {
+\tp := Printer();
 	p.doprint(reflect.NewValue(v).(reflect.StructValue), false, false);
 	s := string(p.buf)[0 : p.n];
-\tp.reset();
 	return s;
 }
 
@@ -134,23 +137,23 @@ func (p *P) sprint(v ...) string {
 // always add spaces between operands, and add a newline
 // after the last operand.
 
-func (p *P) fprintln(w Writer, a ...) (n int, error *os.Error) {
+export func fprintln(w Writer, a ...) (n int, error *os.Error) {
 	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
+\tp := Printer();
 	p.doprint(v, true, true);
 	n, error = w.Write(p.buf[0:p.n]);
-\tp.reset();
 	return n, error;
 }
 
-func (p *P) println(v ...) (n int, errno *os.Error) {
-	n, errno = p.fprintln(os.Stdout, v);
+export func println(v ...) (n int, errno *os.Error) {
+\tn, errno = fprintln(os.Stdout, v);
 	return n, errno;
 }
 
-func (p *P) sprintln(v ...) string {
+export func sprintln(v ...) string {
+\tp := Printer();
 	p.doprint(reflect.NewValue(v).(reflect.StructValue), true, true);
 	s := string(p.buf)[0 : p.n];
-\tp.reset();
 	return s;
 }
 

変更の背景

このコミットの主な背景は、Go言語のfmtパッケージが提供するフォーマット機能において、再帰的な呼び出しを安全かつ効率的に処理できるようにすることです。特に、カスタムフォーマット動詞(fmt.Formatterインターフェースなどを実装した型)が、自身の内部でさらにfmt.Sprintfなどの関数を呼び出すようなシナリオを想定しています。

従来のfmtパッケージの内部実装では、プリンタの状態(P struct)が再利用されていました。これは、パフォーマンスの観点からは有利ですが、あるfmt関数が別のfmt関数を呼び出すような再帰的な状況が発生した場合、同じプリンタの状態を共有することになり、出力バッファの競合や状態の破壊といった問題を引き起こす可能性がありました。

このコミットは、このような再帰的な呼び出しが発生しても、それぞれの呼び出しが独立したプリンタの状態を持つようにすることで、安全性を確保し、fmtパッケージの柔軟性を高めることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の基本的な概念とfmtパッケージの動作に関する知識が必要です。

  1. fmtパッケージ: Go言語の標準ライブラリの一つで、フォーマットされたI/O(入出力)を提供します。Printf, Sprintf, Fprintなどの関数を通じて、様々な型の値を整形して出力したり、文字列に変換したりする機能を提供します。
  2. フォーマット動詞 (Format Verbs): fmtパッケージの関数で使用される%v, %s, %dなどの特殊な記号で、値の表示形式を指定します。
  3. reflectパッケージ: Go言語のリフレクション機能を提供します。実行時に型情報や値の操作を可能にします。fmtパッケージは、引数の型を動的に検査し、適切なフォーマット処理を行うためにreflectパッケージを内部的に利用しています。
  4. インターフェース (Interfaces): Go言語における型が満たすべき振る舞いを定義するものです。fmtパッケージでは、特定のインターフェース(例: Stringer, Formatter, Writer)を実装することで、カスタム型がfmt関数によってどのようにフォーマットされるかを制御できます。
    • Writerインターフェース: Write([]byte) (n int, err error)メソッドを持つインターフェースです。os.Stdoutbytes.Bufferなど、バイト列を書き込むことができるあらゆるものがこのインターフェースを実装しています。fmt.Fprintfなどの関数は、Writerインターフェースを満たす任意のオブジェクトに出力できます。
  5. カスタムフォーマット動詞 (Custom Verbs): ユーザー定義の型がfmt.Formatterインターフェースを実装することで、独自のフォーマットロジックを提供できます。例えば、%xで構造体の特定のフィールドを16進数で表示するといったことが可能です。

技術的詳細

このコミットの技術的な核心は、fmtパッケージの内部プリンタであるP structのインスタンス管理と、Writerインターフェースの実装にあります。

  1. P structの非エクスポート化とPrinter()関数の変更:

    • export type P struct { から type P struct { へ変更され、P structはパッケージ外部から直接アクセスできなくなりました。これは、Pfmtパッケージの内部実装の詳細であることを明確にし、外部からの不適切な利用を防ぐためのカプセル化の一環です。
    • 同様に、export func Printer() *P { から func Printer() *P { へ変更され、Printer()関数も非エクスポート化されました。この関数はPの新しいインスタンスを生成する役割を担いますが、これも内部的なヘルパー関数となりました。
  2. P.reset()メソッドの削除:

    • 以前のバージョンでは、P structは再利用され、各フォーマット操作の前にreset()メソッドを呼び出して内部状態(特にn、書き込まれたバイト数)をリセットしていました。
    • このコミットでは、後述するように各fmt関数呼び出しで新しいPインスタンスが生成されるようになるため、reset()メソッドは不要となり削除されました。
  3. P structへのWriteメソッドの実装:

    • これが最も重要な変更点の一つです。P structに以下のWriteメソッドが追加されました。
      func (p *P) Write(b *[]byte) (ret int, err *os.Error) {
          p.addbytes(b, 0, len(b));
          return len(b), nil;
      }
      
    • このメソッドの追加により、P structはGoの標準Writerインターフェース(Write([]byte) (n int, err error))を満たすようになりました。
    • この変更の意図は、コメントに「Implement Write so we can call fprintf on a P, for recursive use in custom verbs.」(カスタム動詞での再帰的な使用のために、P上でfprintfを呼び出せるようにWriteを実装する)と明記されています。
    • つまり、P自体がWriterとして振る舞えるようになったことで、fmtパッケージの内部で、別のfmt関数(例: fprintf)を呼び出す際に、その出力先として新しいPインスタンスを渡すことが可能になります。これにより、再帰的な呼び出しが独立したプリンタの状態を持つことが保証されます。
  4. 主要なfmt関数(fprintf, printf, sprintfなど)の変更:

    • これらの関数は、以前はP structのメソッドとして定義されていましたが、このコミットでパッケージレベルの関数(export func ...)に変更されました。
    • そして、これらの関数の内部でp := Printer()という行が追加されました。これは、各fmt関数が呼び出されるたびに、新しいPインスタンスを生成することを意味します。
    • これにより、fmt関数が再帰的に呼び出された場合でも、それぞれの呼び出しが独自のPインスタンス(独自の出力バッファと状態)を持つため、互いに干渉することなく安全に処理を進めることができます。
    • p.reset()の呼び出しが削除されたのは、新しいPインスタンスが常にクリーンな状態で開始されるためです。

これらの変更により、fmtパッケージはより堅牢になり、カスタムフォーマットロジック内でfmt関数を再帰的に使用するような高度なシナリオにも対応できるようになりました。

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

変更はすべて src/lib/fmt/print.go ファイル内で行われています。

  1. P structとPrinter()関数のexportキーワード削除:
    -export type P struct {
    +type P struct {
    // ...
    -export func Printer() *P {
    +func Printer() *P {
    
  2. P.reset()メソッドの削除:
    -func (p *P) reset() {
    -	p.n = 0;
    -}// ...
    
  3. P structへのWriteメソッドの追加:
    // Implement Write so we can call fprintf on a P, for
    // recursive use in custom verbs.
    func (p *P) Write(b *[]byte) (ret int, err *os.Error) {
    	p.addbytes(b, 0, len(b));
    	return len(b), nil;
    }
    
  4. 主要なfmt関数の変更(例: fprintf:
    • メソッドからパッケージレベル関数への変更 (func (p *P) fprintf -> export func fprintf)
    • p := Printer() の追加
    • p.reset() の削除
    -func (p *P) fprintf(w Writer, format string, a ...) (n int, error *os.Error) {
    +export func fprintf(w Writer, format string, a ...) (n int, error *os.Error) {
     	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
    +\tp := Printer(); // 新しいPインスタンスを生成
     	p.doprintf(format, v);
     	n, error = w.Write(p.buf[0:p.n]);
    -\tp.reset(); // resetの削除
     	return n, error;
     }
    
    同様の変更が printf, sprintf, fprint, print, sprint, fprintln, println, sprintln の各関数にも適用されています。

コアとなるコードの解説

このコミットの核心は、fmtパッケージの内部プリンタPが、再帰的なフォーマット操作において独立した状態を保つように設計された点です。

以前は、fmtパッケージの関数が内部でPインスタンスを共有し、reset()メソッドで状態をクリアして再利用していました。しかし、これはカスタムフォーマット動詞などがfmt.Sprintfなどを呼び出すような再帰的なシナリオでは問題を引き起こす可能性がありました。例えば、あるfmt.Sprintfの呼び出し中に、その内部で別のfmt.Sprintfが呼び出されると、両者が同じPインスタンスの出力バッファを操作しようとし、結果が混ざったり壊れたりする恐れがありました。

このコミットでは、この問題を解決するために以下の戦略が取られました。

  1. PWriterインターフェース実装: P structにWrite([]byte) (int, error)メソッドが追加されました。これにより、PはGoの標準io.Writerインターフェースを満たすようになります。これは、Pインスタンス自体がバイト列の書き込み先として機能できることを意味します。
  2. fmt関数呼び出しでの新しいPインスタンス生成: fprintf, printf, sprintfなどの主要なfmt関数は、呼び出されるたびにp := Printer()という行で新しいPインスタンスを生成するようになりました。これにより、各トップレベルのfmt関数呼び出しは、独自のクリーンなプリンタ状態(出力バッファなど)を持つことになります。
  3. 再帰的な呼び出しの分離:
    • もし、あるカスタムフォーマット動詞がfmt.Sprintfを呼び出したとします。このfmt.Sprintfは、新しいPインスタンスを生成して処理を開始します。
    • この新しいPインスタンスは、Writerインターフェースを実装しているため、そのWriteメソッドを通じてバイト列を受け取ることができます。
    • これにより、外側のfmt呼び出しと内側のfmt呼び出しが、それぞれ独立したPインスタンス上で動作し、出力バッファの競合がなくなります。

この設計変更により、fmtパッケージはより複雑なフォーマットシナリオ、特に再帰的なフォーマット要求に対して、堅牢かつ予測可能な振る舞いを提供するようになりました。P structとPrinter()関数が非エクスポート化されたのは、これらの変更がfmtパッケージの内部実装の詳細であり、外部から直接操作されるべきではないという設計思想を反映しています。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(src/lib/fmt/print.go
  • Go言語のコミット履歴
  • Go言語のfmtパッケージに関する一般的な解説記事やチュートリアル(再帰的なprintfの概念を理解するため)