[インデックス 1073] ファイルの概要
このコミットは、Go言語の標準ライブラリであるfmt
パッケージ内のprint.go
ファイルに対する変更です。具体的には、値がString()
メソッドを実装している場合に、fmt.print
系の関数(fmt.printf
系ではない)がそのString()
メソッドの出力を利用するように挙動を変更しています。これにより、カスタムの文字列表現を持つ型が、より自然な形で出力されるようになります。
コミット
commit 91212bd1ad81d30425bdb0b0f5d813369490c9bb
Author: Rob Pike <r@golang.org>
Date: Thu Nov 6 11:38:44 2008 -0800
If a value implements String(), use that in fmt.print (not fmt.printf)
R=rsc
DELTA=13 (9 added, 0 deleted, 4 changed)
OCL=18682
CL=18684
---
src/lib/fmt/print.go | 17 +++++++++++++----\n 1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/src/lib/fmt/print.go b/src/lib/fmt/print.go
index 3c237f5e32..3516b19ab9 100644
--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -22,14 +22,18 @@ export type Writer interface {
// Representation of printer state passed to custom formatters.
// Provides access to the Writer interface plus information about
// the active formatting verb.
-export type FormatHelper interface {\n+export type Formatter interface {\n \tWrite(b *[]byte) (ret int, err *os.Error);\n \tWidth()\t(wid int, ok bool);\n \tPrecision()\t(prec int, ok bool);\n }\n \n-export type Formatter interface {\n-\tFormat(f FormatHelper, c int);\n+export type Format interface {\n+\tFormat(f Formatter, c int);\n+}\n+\n+export type String interface {\n+\tString() string\n }\n \n const Runeself = 0x80\n@@ -303,7 +307,7 @@ func (p *P) doprintf(format string, v reflect.StructValue) {\n \t\t}\n \t\tfield := v.Field(fieldnum);\n \t\tfieldnum++;\n-\t\tif formatter, ok := field.Interface().(Formatter); ok {\n+\t\tif formatter, ok := field.Interface().(Format); ok {\n \t\t\tformatter.Format(p, c);\n \t\t\tcontinue;\n \t\t}\n@@ -439,6 +443,11 @@ func (p *P) doprint(v reflect.StructValue, addspace, addnewline bool) {\n \t\t\t\tp.add(\' \')\n \t\t\t}\n \t\t}\n+\t\tif stringer, ok := field.Interface().(String); ok {\n+\t\t\tp.addstr(stringer.String());\n+\t\t\tprev_string = false;\t// this value is not a string\n+\t\t\tcontinue;\n+\t\t}\n \t\tswitch field.Kind() {\n \t\tcase reflect.BoolKind:\n \t\t\ts = p.fmt.boolean(field.(reflect.BoolValue).Get()).str();\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/91212bd1ad81d30425bdb0b0f5d813369490c9bb](https://github.com/golang/go/commit/91212bd1ad81d30425bdb0b0f5d813369490c9bb)
## 元コミット内容
このコミットの元のメッセージは以下の通りです。
> If a value implements String(), use that in fmt.print (not fmt.printf)
> R=rsc
> DELTA=13 (9 added, 0 deleted, 4 changed)
> OCL=18682
> CL=18684
これは、「もし値が`String()`メソッドを実装している場合、`fmt.print`系の関数(`fmt.printf`系ではない)でそのメソッドを使用する」という変更の意図を簡潔に示しています。
## 変更の背景
Go言語の`fmt`パッケージは、様々な型の値を整形して出力するための機能を提供します。初期のGo言語では、値の文字列表現を得るための統一されたメカニズムがまだ完全に確立されていませんでした。このコミットが行われた2008年11月は、Go言語が一般に公開される直前の時期であり、言語の基本的な設計や標準ライブラリのAPIが活発に開発・洗練されていた段階です。
この変更の背景には、以下のような課題があったと考えられます。
1. **直感的な出力の欠如**: ユーザー定義型を`fmt.Print`や`fmt.Println`で出力しようとした際、その型の内部表現がそのまま出力されてしまい、人間が読みやすい形式ではなかった可能性があります。例えば、構造体のアドレスやフィールドのデフォルトの文字列表現が表示されるなどです。
2. **`fmt.printf`との挙動の不一致**: `fmt.printf`はフォーマット指定子(例: `%s`)を通じて、値の文字列表現を制御できます。しかし、`fmt.print`や`fmt.println`のような単純な出力関数では、そのような制御ができませんでした。開発者は、`String()`メソッドを実装することで、その型の「自然な」文字列表現を提供したいと考えるのが一般的です。このコミットは、その期待に応えるためのものです。
3. **`Stringer`インターフェースの導入**: Go言語の設計思想の一つに「インターフェースによるポリモーフィズム」があります。特定のメソッドシグネチャを持つインターフェースを導入することで、そのインターフェースを満たす任意の型に対して共通の処理を適用できるようになります。`String()`メソッドを持つ型を特別扱いするための標準的なインターフェース(後の`fmt.Stringer`)の必要性が認識され、その初期の実装としてこの変更が導入されました。
この変更により、開発者は自身の型に`String() string`メソッドを実装するだけで、`fmt.Print`や`fmt.Println`がそのカスタム表現を自動的に使用するようになり、より直感的で読みやすいデバッグ出力やログ出力が可能になりました。
## 前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と、当時の`fmt`パッケージの動作に関する知識が必要です。
1. **インターフェース (Interfaces)**:
Go言語におけるインターフェースは、メソッドのシグネチャの集合を定義する型です。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たすとみなされます。このコミットでは、`String`インターフェースと`Format`インターフェースが関わっています。
- `String`インターフェース: このコミットで新しく導入された`String`インターフェースは、`String() string`というメソッドを一つだけ持ちます。これは、Go言語の標準ライブラリで広く使われる`fmt.Stringer`インターフェースの原型です。このインターフェースを実装する型は、自身の文字列表現を返す責任を持ちます。
- `Format`インターフェース: `fmt.Formatter`インターフェースの原型であり、`Format(f Formatter, c int)`メソッドを持ちます。これは、`fmt.Printf`のようなフォーマット指定子(例: `%v`, `%s`など)を使って値を整形する際に、カスタムの整形ロジックを提供するためのものです。`c`はフォーマット動詞(例: `'v'`, `'s'`)を表します。
2. **`fmt`パッケージ**:
Go言語の標準入出力フォーマットを提供するパッケージです。
- `fmt.Print`, `fmt.Println`: 引数をデフォルトのフォーマットで出力します。`Println`は最後に改行を追加します。これらの関数は、引数の型に応じて適切な文字列表現を自動的に選択しようとします。
- `fmt.Printf`: フォーマット文字列と引数を受け取り、指定されたフォーマットで出力します。フォーマット指定子(例: `%s`、`%d`、`%v`など)を使って、出力形式を細かく制御できます。
このコミットの重要な点は、`String()`メソッドの利用が`fmt.print`系に限定され、`fmt.printf`系には適用されないことです。これは、`printf`系が明示的なフォーマット指定子によって出力形式を制御するため、`String()`メソッドによる暗黙的な変換を避けるためと考えられます。
3. **`reflect`パッケージ**:
Go言語の`reflect`パッケージは、実行時にプログラムの構造(型、値、メソッドなど)を検査・操作するための機能を提供します。
- `reflect.StructValue`: 当時の`reflect`パッケージにおける構造体の値を表す型です。現在の`reflect.Value`に相当します。
- `field.Interface()`: `reflect`パッケージの`Value`型(当時の`StructValue`のフィールド)が持つメソッドで、その値が保持するインターフェースの値を返します。
- 型アサーション (`value.(Type)`): インターフェースの値が特定の型(または別のインターフェース)を実装しているかどうかをチェックし、その型に変換します。`value.(Type)`は、変換に成功すればその型の値と`true`を、失敗すればゼロ値と`false`を返します。このコミットでは、`field.Interface().(String)`や`field.Interface().(Format)`のように使われています。
- `field.Kind()`: 値の基本的な種類(例: `reflect.BoolKind`, `reflect.IntKind`, `reflect.StringKind`など)を返します。
4. **Go言語の初期の構文**:
コミットのコードスニペットを見ると、現在のGo言語とは異なる初期の構文がいくつか見られます。
- `export type ... interface { ... }`: 現在の`type ... interface { ... }`に相当します。`export`キーワードは、パッケージ外に公開される型であることを示していました(現在のGoでは大文字で始まる識別子が公開されます)。
- `*[]byte`: 現在の`[]byte`に相当します。スライスは当初、ポインタとして扱われていました。
- `os.Error`: 現在の`error`インターフェースに相当します。
これらの知識を前提として、コミットの変更内容を詳細に見ていきます。
## 技術的詳細
このコミットの主要な技術的変更点は、`fmt`パッケージの内部で値の文字列表現を決定するロジックに、新しく導入された`String`インターフェース(現在の`fmt.Stringer`)によるチェックを追加したことです。
`fmt`パッケージのプリンタ(`P`構造体)は、`doprint`という内部関数で引数の値を処理し、その文字列表現を生成します。この関数は、引数の型に応じて様々な処理を行います。
変更前は、`doprint`関数はまず`Format`インターフェース(当時の`Formatter`インターフェース)を実装しているかをチェックし、次にプリミティブ型(bool, int, stringなど)の処理に移っていました。
変更後は、`Format`インターフェースのチェックの前に、**`String`インターフェースを実装しているかどうかのチェックが追加されました**。
1. **`String`インターフェースの定義**:
```go
export type String interface {
String() string
}
```
このインターフェースが`src/lib/fmt/print.go`に追加されました。これは、Go言語でカスタムの文字列表現を提供するための標準的な方法となる`fmt.Stringer`インターフェースの直接の祖先です。
2. **`Formatter`と`FormatHelper`の名称変更**:
コミットのdiffを見ると、`FormatHelper`が`Formatter`に、そして`Formatter`が`Format`にそれぞれ名称変更されています。これは、APIの命名規則をより明確にするためのリファクタリングであり、`String()`メソッドの導入とは直接関係ありませんが、同じコミットで同時に行われています。
- 旧: `export type FormatHelper interface { ... }`
- 新: `export type Formatter interface { ... }` (旧`FormatHelper`が`Formatter`に)
- 旧: `export type Formatter interface { Format(f FormatHelper, c int); }`
- 新: `export type Format interface { Format(f Formatter, c int); }` (旧`Formatter`が`Format`に、引数の型も新`Formatter`に)
3. **`doprint`関数における`String`インターフェースの優先**:
`doprint`関数内で、`reflect.StructValue`から取得した`field`(値)に対して、以下の新しいロジックが追加されました。
```go
if stringer, ok := field.Interface().(String); ok {
p.addstr(stringer.String());
prev_string = false; // this value is not a string
continue;
}
```
これは、`field`が`String`インターフェースを実装している場合、その`String()`メソッドを呼び出し、その結果の文字列をプリンタのバッファに追加するというものです。この処理は、既存の`switch field.Kind()`によるプリミティブ型の処理よりも前に実行されます。これにより、カスタムの`String()`メソッドが定義されている場合は、それが優先的に使用されるようになります。
`prev_string = false;`という行は、前の値が文字列であったかどうかを追跡する内部フラグをリセットしています。これは、`fmt.Print`や`fmt.Println`が引数間にスペースを追加する際の挙動を制御するためのものです。`String()`メソッドによって生成された出力は、Goの組み込み文字列型とは異なる「カスタム文字列」として扱われるため、このフラグを`false`に設定することで、後続の出力との間にスペースが適切に挿入されるようにしています。
この変更は、`fmt.print`系の関数にのみ適用され、`fmt.printf`系の関数(`doprintf`)には適用されません。`doprintf`はフォーマット指定子に基づいて厳密に動作するため、`String()`メソッドによる暗黙的な変換は行われません。これは、`printf`が明示的な制御を目的としているため、予期せぬ挙動を防ぐための設計判断です。
## コアとなるコードの変更箇所
変更は`src/lib/fmt/print.go`ファイルに集中しています。
1. **新しいインターフェースの定義**:
```diff
--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -22,14 +22,18 @@ export type Writer interface {
// Representation of printer state passed to custom formatters.
// Provides access to the Writer interface plus information about
// the active formatting verb.
-export type FormatHelper interface {\n+export type Formatter interface {\n Write(b *[]byte) (ret int, err *os.Error);\n Width()\t(wid int, ok bool);\n Precision()\t(prec int, ok bool);\n }\n \n -export type Formatter interface {\n -\tFormat(f FormatHelper, c int);\n +export type Format interface {\n +\tFormat(f Formatter, c int);\n +}\n +\n +export type String interface {\n +\tString() string\n }\n \n const Runeself = 0x80
```
`String`インターフェースが追加され、既存の`FormatHelper`と`Formatter`インターフェースがそれぞれ`Formatter`と`Format`にリネームされています。
2. **`doprint`関数内のロジック変更**:
```diff
--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -439,6 +443,11 @@ func (p *P) doprint(v reflect.StructValue, addspace, addnewline bool) {
\tp.add(' ')
}
}
+\t\tif stringer, ok := field.Interface().(String); ok {
+\t\t\tp.addstr(stringer.String());
+\t\t\tprev_string = false;\t// this value is not a string
+\t\t\tcontinue;\n+\t\t}\n \tswitch field.Kind() {
\t\tcase reflect.BoolKind:
\t\t\ts = p.fmt.boolean(field.(reflect.BoolValue).Get()).str();
```
`doprint`関数内で、`field.Interface().(String)`による型アサーションが追加され、`String`インターフェースを実装している場合はその`String()`メソッドの出力が優先されるようになりました。
3. **`doprintf`関数内の型アサーションの更新**:
```diff
--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -303,7 +307,7 @@ func (p *P) doprintf(format string, v reflect.StructValue) {
}\n \t\tfield := v.Field(fieldnum);\n \t\tfieldnum++;\n -\t\tif formatter, ok := field.Interface().(Formatter); ok {\n +\t\tif formatter, ok := field.Interface().(Format); ok {\n \t\t\tformatter.Format(p, c);\n \t\t\tcontinue;\n \t\t}\n ```
`doprintf`関数内では、`Formatter`インターフェースが`Format`にリネームされたことに伴い、型アサーションの対象が`Format`に更新されています。`String`インターフェースのチェックはここには追加されていません。
## コアとなるコードの解説
このコミットの核心は、`fmt`パッケージが値を文字列表現に変換する際の優先順位に`String()`メソッドを実装した型を追加した点にあります。
`doprint`関数は、`fmt.Print`や`fmt.Println`のような関数が内部的に呼び出す、値の出力処理を担う関数です。この関数は、与えられた値(`field`)をどのように文字列表現にするかを決定するために、いくつかのチェックを順に行います。
変更前は、このチェックの順序は以下のようでした(簡略化)。
1. 値が`Formatter`インターフェース(現在の`fmt.Formatter`の原型)を実装しているか?
2. 値の基本的な種類(`Kind`)に基づいて、組み込みの型(bool, int, stringなど)の処理を行う。
このコミットによって、この順序に新しいステップが追加されました。
1. 値が`String`インターフェース(現在の`fmt.Stringer`の原型)を実装しているか?
- **もし実装していれば、その`String()`メソッドを呼び出し、その結果を直接出力する。**
- この場合、それ以降のチェック(`Format`インターフェースや組み込み型の処理)はスキップされる。
2. 値が`Format`インターフェース(現在の`fmt.Formatter`の原型)を実装しているか?
3. 値の基本的な種類(`Kind`)に基づいて、組み込みの型(bool, int, stringなど)の処理を行う。
この変更により、開発者は自身のカスタム型に`String() string`メソッドを実装するだけで、`fmt.Print`や`fmt.Println`がそのカスタム表現を自動的に使用するようになります。これは、Go言語における「慣習によるインターフェースの実装」(implicit interfaces)の強力な例であり、コードの可読性とデバッグのしやすさを大幅に向上させました。
例えば、以下のようなカスタム型があったとします(現在のGo構文で記述)。
```go
type MyType struct {
Value int
}
func (m MyType) String() string {
return fmt.Sprintf("MyType with value: %d", m.Value)
}
func main() {
m := MyType{Value: 42}
fmt.Println(m) // 変更前は "{42}" のような出力、変更後は "MyType with value: 42"
}
このコミット以前は、fmt.Println(m)
はMyType
のデフォルトの文字列表現(例えば、構造体のアドレスやフィールドのデフォルト値)を出力していたでしょう。しかし、このコミット以降は、MyType
がString()
メソッドを実装しているため、fmt.Println
は自動的にそのString()
メソッドを呼び出し、より意味のある「MyType with value: 42」という出力を生成するようになります。
この機能は、Go言語のデバッグ、ロギング、およびユーザーフレンドリーな出力において非常に重要な役割を果たしています。
関連リンク
- Go言語の
fmt
パッケージのドキュメント: https://pkg.go.dev/fmt - Go言語の
reflect
パッケージのドキュメント: https://pkg.go.dev/reflect fmt.Stringer
インターフェースに関するGoの公式ブログ記事(関連する概念の解説): https://go.dev/blog/strings
参考にした情報源リンク
- Go言語の公式ドキュメント (
fmt
,reflect
パッケージ) - Go言語の初期のコミット履歴と設計に関する議論(GoのGitHubリポジトリ)
- Go言語のブログ記事やチュートリアル(
Stringer
インターフェースの概念理解のため) - Go言語の歴史に関する一般的な知識
- Web検索: "Go fmt package Stringer interface", "Go fmt.Print vs fmt.Printf", "Go reflect package early versions"