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

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

このコミットは、Go言語の標準ライブラリであるfmtパッケージにおける、printf系の関数とprint系の関数の実装を改善するものです。特に、可変長引数(variadic arguments)の扱いと、構造体(struct)の値の出力機能に焦点を当てています。初期のGo言語におけるfmtパッケージの設計思想と、可変長引数の内部的な処理に関する重要な変更が含まれています。

コミット

  • コミットハッシュ: 2d4f7ba0cd65dfd9b47b3641f24b759c627c9433
  • Author: Rob Pike r@golang.org
  • Date: Sun Nov 2 12:33:02 2008 -0800

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

https://github.com/golang/go/commit/2d4f7ba0cd65dfd9b47b3641f24b759c627c9433

元コミット内容

printf as we know and love it.

Plus print[ln] with the ability to print struct values.

Note for language mavens: if a "..." function passes its argument
to another "..." function, the argument is not wrapped again. This
allows printf to call fprintf without extra manipulation. It's good
but needs to go in the spec.

This code works:
///
package main

import fmt "fmt"
import os "os"

type T struct { s string; a, b int }

func main() {
        P := fmt.Printer();
        P.printf("%s = %d with float value %.4f\n", "hi there", 7, 123.456);
        P.println("hi there", 7, 123.456);
        P.fprintf(os.Stdout, "%s = %d with float value %.4f\n", "hi there", 7, 123.456);
        P.println(T{"x", 7, 234}, "end of struct", 8, 9);
}

R=rsc
DELTA=28  (7 added, 3 deleted, 18 changed)
OCL=18321
CL=18324

変更の背景

このコミットは、Go言語がまだ開発の初期段階にあった2008年に行われたものです。Go言語の設計目標の一つに、C言語のようなシステムプログラミング言語の効率性と、より現代的な言語の安全性と生産性を両立させることがありました。その中で、C言語のprintfに代表されるような柔軟なフォーマット出力機能は不可欠でした。

初期のGo言語では、可変長引数(variadic arguments)の扱いがまだ確立されておらず、reflect.Emptyという型が可変長引数のプレースホルダーとして使われていたようです。しかし、これはGo言語の可変長引数の本来の意図とは異なり、より自然で効率的な...(エリプシス)構文への移行が必要でした。

また、fmtパッケージは、単に値を文字列に変換するだけでなく、構造体の内容を適切に表示する機能も求められていました。特に、printprintlnのような関数で構造体を直接渡した場合に、その内部のフィールドも出力できるようにすることが目標でした。

このコミットのもう一つの重要な背景は、可変長引数を持つ関数が、その引数を別の可変長引数を持つ関数に渡す際の挙動に関するものです。C言語のprintffprintfを呼び出すように、Goでも同様のパターンを効率的に実現するためには、引数が不必要に「ラップ」されないようにする必要がありました。これは言語仕様として明確に定義されるべき重要な挙動であり、このコミットはその挙動をコードで示し、将来の仕様策定に向けた布石となっています。

前提知識の解説

Go言語の可変長引数(Variadic Functions)

Go言語では、関数の最後のパラメータに...(エリプシス)を付けることで、その関数が可変個の引数を受け取れるようになります。関数内部では、この可変長引数は指定された型のスライスとして扱われます。

例:

func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

このsum関数は、sum(1, 2, 3)のように複数の整数を渡すことも、sum()のように引数なしで呼び出すことも可能です。

reflectパッケージと初期のGo言語におけるreflect.Empty

Go言語のreflectパッケージは、実行時にプログラムの型情報を検査したり、値を操作したりするための機能を提供します。このコミットが行われた2008年頃のGo言語はまだ開発初期であり、現在のGo言語とは異なるAPIや概念が存在していました。

コミットの差分を見ると、reflect.Emptyという型が引数として使われています。現在のGo言語にはreflect.Emptyという型は存在しません。これは、初期のGo言語における可変長引数の実装がまだ固まっておらず、reflect.Emptyが可変長引数のプレースホルダーとして一時的に使用されていたことを示唆しています。

現在のGo言語では、可変長引数は直接...Typeの形で定義され、関数内部では[]Typeのスライスとして扱われます。reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue)のような記述は、当時のリフレクションAPIを使って、渡された引数(おそらくreflect.Emptyでラップされたもの)から実際の値(この場合は構造体)を取り出すための処理と考えられます。

fmtパッケージ

fmtパッケージは、Go言語におけるフォーマットされたI/O(入出力)を扱うための標準ライブラリです。C言語のprintfscanfに似た機能を提供し、文字列のフォーマット、標準出力への出力、ファイルへの出力など、様々な用途で利用されます。

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

  • fmt.Print, fmt.Println, fmt.Printf: 標準出力への出力
  • fmt.Fprint, fmt.Fprintln, fmt.Fprintf: 指定されたio.Writerへの出力(例: ファイル、ネットワーク接続)
  • fmt.Sprint, fmt.Sprintln, fmt.Sprintf: フォーマットされた文字列を返す

このコミットでは、fmt.Printerという構造体(現在のfmtパッケージには直接存在しないが、内部的なプリンターの概念)のメソッドとして、これらの機能が実装されています。

技術的詳細

このコミットの技術的な詳細は、主にsrc/lib/fmt/print.goファイルにおける変更に集約されています。

  1. reflect.Emptyから...への移行: fprintf, printf, sprintf, fprint, print, sprint, fprintln, println, sprintlnといった全ての出力関数の引数リストが、v reflect.Emptyからv ...へと変更されています。これは、Go言語の可変長引数の構文が...に統一され、より直感的で標準的な方法に移行したことを示しています。

  2. doprint関数の引数変更と機能拡張:

    • func (p *P) doprint(v reflect.StructValue, addspace bool)func (p *P) doprint(v reflect.StructValue, addspace, addnewline bool)に変更されました。
    • addspaceは、引数間にスペースを追加するかどうかを制御します。
    • addnewlineは、出力の最後に改行を追加するかどうかを制御します。これにより、println系の関数が改行を自動的に追加する挙動を、doprint関数内で一元的に制御できるようになりました。
  3. doprintにおけるスペースと改行の追加ロジックの改善:

    • 以前はis_printlnという単一のフラグでスペースと改行の挙動を制御していましたが、addspaceaddnewlineの2つのフラグに分離されたことで、より柔軟かつ正確な制御が可能になりました。
    • 特に、println系の関数では常にスペースを追加し、print系の関数では文字列でないオペランドの間にのみスペースを追加するというロジックが明確化されました。
  4. 構造体(Struct)の出力対応:

    • doprint関数内のswitch field.Kind()case reflect.StructKind:が追加されました。
    • これにより、出力対象のフィールドが構造体である場合、{で始まり、その構造体の内容を再帰的にdoprintで出力し、}で閉じるという処理が追加されました。これにより、println(T{"x", 7, 234}, "end of struct", 8, 9);のようなコードで構造体の内容が適切に表示されるようになりました。
  5. 可変長引数の「ラップされない」挙動の明示: コミットメッセージの「Note for language mavens: if a "..." function passes its argument to another "..." function, the argument is not wrapped again. This allows printf to call fprintf without extra manipulation. It's good but needs to go in the spec.」という記述は非常に重要です。 これは、Go言語の可変長引数を持つ関数(例: printf)が、受け取った可変長引数をそのまま別の可変長引数を持つ関数(例: fprintf)に渡す際に、引数が不必要にスライスとして再ラップされないことを意味します。これにより、パフォーマンスのオーバーヘッドが削減され、より自然な関数呼び出しの連鎖が可能になります。この挙動は、Go言語の設計における重要な決定であり、後の言語仕様に明記されるべき点として言及されています。

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

--- a/src/lib/fmt/print.go
+++ b/src/lib/fmt/print.go
@@ -83,11 +83,11 @@ export type Writer interface {
 }
 
 func (p *P) doprintf(format string, v reflect.StructValue);
-func (p *P) doprint(v reflect.StructValue, addspace bool);
+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 reflect.Empty) (n int, error *os.Error) {
+func (p *P) fprintf(w Writer, format string, a ...) (n int, error *os.Error) {
 	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
 	p.doprintf(format, v);
 	n, error = w.Write(p.buf[0:p.n]);
@@ -95,12 +95,12 @@ func (p *P) fprintf(w Writer, format string, a reflect.Empty) (n int, error *os.
 	return n, error;
 }
 
-func (p *P) printf(format string, v reflect.Empty) (n int, errno *os.Error) {
+func (p *P) printf(format string, v ...) (n int, errno *os.Error) {
 	n, errno = p.fprintf(os.Stdout, format, v);
 	return n, errno;
 }
 
-func (p *P) sprintf(format string, v reflect.Empty) string {
+func (p *P) sprintf(format string, v ...) string {
 	p.doprintf(format, reflect.NewValue(v).(reflect.StructValue));
 	s := string(p.buf)[0 : p.n];
 	p.reset();
@@ -110,21 +110,21 @@ func (p *P) sprintf(format string, v reflect.Empty) string {
 // 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 reflect.Empty) (n int, error *os.Error) {
+func (p *P) fprint(w Writer, a ...) (n int, error *os.Error) {
 	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
-\tp.doprint(v, false);\n+\tp.doprint(v, false, false);\n \tn, error = w.Write(p.buf[0:p.n]);\n \tp.reset();\n \treturn n, error;\n }\n \n-func (p *P) print(v reflect.Empty) (n int, errno *os.Error) {\n+func (p *P) print(v ...) (n int, errno *os.Error) {\n \tn, errno = p.fprint(os.Stdout, v);\n \treturn n, errno;\n }\n \n-func (p *P) sprint(v reflect.Empty) string {\n-\tp.doprint(reflect.NewValue(v).(reflect.StructValue), false);\n+func (p *P) sprint(v ...) string {\n+\tp.doprint(reflect.NewValue(v).(reflect.StructValue), false, false);\n \ts := string(p.buf)[0 : p.n];\n \tp.reset();\n \treturn s;\n@@ -134,21 +134,21 @@ func (p *P) sprint(v reflect.Empty) string {\n // always add spaces between operands, and add a newline\n // after the last operand.\n \n-func (p *P) fprintln(w Writer, a reflect.Empty) (n int, error *os.Error) {
+func (p *P) fprintln(w Writer, a ...) (n int, error *os.Error) {
 	v := reflect.NewValue(a).(reflect.PtrValue).Sub().(reflect.StructValue);
-\tp.doprint(v, true);\n+\tp.doprint(v, true, true);\n \tn, error = w.Write(p.buf[0:p.n]);\n \tp.reset();\n \treturn n, error;\n }\n \n-func (p *P) println(v reflect.Empty) (n int, errno *os.Error) {\n+func (p *P) println(v ...) (n int, errno *os.Error) {\n \tn, errno = p.fprintln(os.Stdout, v);\n \treturn n, errno;\n }\n \n-func (p *P) sprintln(v reflect.Empty) string {\n-\tp.doprint(reflect.NewValue(v).(reflect.StructValue), true);\n+func (p *P) sprintln(v ...) string {\n+\tp.doprint(reflect.NewValue(v).(reflect.StructValue), true, true);\n \ts := string(p.buf)[0 : p.n];\n \tp.reset();\n \treturn s;\n@@ -362,19 +362,19 @@ func (p *P) doprintf(format string, v reflect.StructValue) {\n 	}\n }\n \n-func (p *P) doprint(v reflect.StructValue, is_println bool) {\n+func (p *P) doprint(v reflect.StructValue, addspace, addnewline bool) {\n 	prev_string := false;\n 	for fieldnum := 0; fieldnum < v.Len();  fieldnum++ {\n \t\t// always add spaces if we're doing println\n \t\tfield := v.Field(fieldnum);\n \t\ts := "";\n-\t\tif is_println {\n-\t\t\tif fieldnum > 0 {\n+\t\tif fieldnum > 0 {\n+\t\t\tif addspace {\n+\t\t\t\tp.add(' ')\n+\t\t\t} else if field.Kind() != reflect.StringKind && !prev_string{\n+\t\t\t\t// if not doing println, add spaces if neither side is a string\n \t\t\t\tp.add(' ')\n \t\t\t}\n-\t\t} else if field.Kind() != reflect.StringKind && !prev_string{\n-\t\t\t// if not doing println, add spaces if neither side is a string\n-\t\t\tp.add(' ')\n \t\t}\n \t\tswitch field.Kind() {\n \t\tcase reflect.BoolKind:\n@@ -396,13 +396,17 @@ func (p *P) doprint(v reflect.StructValue, is_println bool) {\n \t\t\tp.add('0');\n \t\t\tp.add('x');\n \t\t\ts = p.fmt.uX64(v).str();\n+\t\tcase reflect.StructKind:\n+\t\t\tp.add('{');\n+\t\t\tp.doprint(field, true, false);\n+\t\t\tp.add('}');\n \t\tdefault:\n \t\t\ts = "???";\n \t\t}\n \t\tp.addstr(s);\n \t\tprev_string = field.Kind() == reflect.StringKind;\n \t}\n-\tif is_println {\n+\tif addnewline {\n \t\tp.add('\\n')\n \t}\n }\n```

## コアとなるコードの解説

### 関数シグネチャの変更

-   **`doprint`関数のシグネチャ変更**:
    -   `- func (p *P) doprint(v reflect.StructValue, addspace bool);`
    -   `+ func (p *P) doprint(v reflect.StructValue, addspace, addnewline bool);`
    `doprint`関数は、出力の内部処理を行うヘルパー関数です。以前は`addspace`という単一のブール値でスペースの追加を制御していましたが、この変更により`addnewline`という新しいブール値が追加され、改行の追加もこの関数で制御できるようになりました。これにより、`print`系と`println`系の関数の挙動をより細かく、かつ一元的に管理できるようになります。

-   **`fprintf`, `printf`, `sprintf`などの引数変更**:
    -   `- func (p *P) fprintf(w Writer, format string, a reflect.Empty) (n int, error *os.Error) {`
    -   `+ func (p *P) fprintf(w Writer, format string, a ...) (n int, error *os.Error) {`
    同様に、`printf`系の関数や`print`系の関数(`fprint`, `print`, `sprint`, `fprintln`, `println`, `sprintln`)の引数も、`reflect.Empty`からGo言語の標準的な可変長引数構文である`...`に変更されています。これは、Go言語の可変長引数の実装が成熟し、より簡潔な構文が採用されたことを示しています。

### `doprint`関数の内部ロジックの変更

-   **スペース追加ロジックの改善**:
    ```diff
    - 	if is_println {
    - 		if fieldnum > 0 {
    - 			p.add(' ')
    - 		}
    - 	} else if field.Kind() != reflect.StringKind && !prev_string{
    - 		// if not doing println, add spaces if neither side is a string
    - 		p.add(' ')
    - 	}
    + 	if fieldnum > 0 {
    + 		if addspace {
    + 			p.add(' ')
    + 		} else if field.Kind() != reflect.StringKind && !prev_string{
    + 			// if not doing println, add spaces if neither side is a string
    + 			p.add(' ')
    + 		}
    + 	}
    ```
    以前は`is_println`というフラグでスペースの追加を制御していましたが、新しいロジックでは`addspace`と`addnewline`の2つのフラグを使用します。
    -   `fieldnum > 0`の場合(最初のフィールド以外):
        -   `addspace`が`true`の場合(`println`系の場合など)、常にスペースを追加します。
        -   `addspace`が`false`の場合(`print`系の場合など)、フィールドが文字列型でなく、かつ前のフィールドも文字列型でなかった場合にのみスペースを追加します。これにより、`"hello"world`のような不自然な結合を防ぎ、`"hello" "world"`のようにスペースを挿入します。

-   **構造体出力の追加**:
    ```diff
    + 		case reflect.StructKind:
    + 			p.add('{');
    + 			p.doprint(field, true, false);
    + 			p.add('}');
    ```
    `doprint`関数内の`switch`文に`reflect.StructKind`のケースが追加されました。これにより、出力対象のフィールドが構造体である場合、`{`で始まり、その構造体の内容を再帰的に`doprint`で出力し、`}`で閉じるという処理が実装されました。`p.doprint(field, true, false)`は、構造体の内部要素を`println`のようにスペース区切りで出力し、最後に改行は追加しないことを意味します。

-   **改行追加ロジックの変更**:
    ```diff
    - 	if is_println {
    + 	if addnewline {
    		p.add('\n')
    	}
    ```
    `doprint`関数の最後に、`is_println`の代わりに`addnewline`フラグを使用して改行を追加するかどうかを決定するように変更されました。これにより、`println`系の関数が自動的に改行を追加する挙動が、`doprint`関数内でより明確に制御されるようになりました。

これらの変更により、Go言語の`fmt`パッケージは、より柔軟で強力な出力機能を提供できるようになり、特に可変長引数の扱いと構造体の出力において、現代のGo言語の`fmt`パッケージの基礎が築かれました。

## 関連リンク

- GitHubコミットページ: https://github.com/golang/go/commit/2d4f7ba0cd65dfd9b47b3641f24b759c627c9433

## 参考にした情報源リンク

- Go言語の可変長引数:
    - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF7yHmsaHORdQILUQt2IcKmrKe0RqrwciN4FmBJZA0wB3-6V_VNd2DVdiBDsGypoE85vIq6EiXON973InAMMkFQbPS4bjDD71GN5y7TnVY1pdlZgT1o3eXCCHnaHMSy8RpXHPBKS6awP-wgvfwMDC07q1KBYitANxJguXEkH_IF2U_iy4JIKLs2A8P-w==
    - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG4w8JNmgku2m7YuPPvobnEAICZ-PNmvxCZINNAFLyTVtKKQEJAKnD0oAx4xOQsZAv56V6NJ6i3GB2e4FdDolNdwGpPUiAC7FGVE5JRSDI4sonQLCelr0Whdx9RjIJzdAHgO5Djg5__IPRqwbr3Fe37sKvIUPp89sUNfwAEDiOlFm0qITa9hXKLnLrBbvwZLg==
    - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHbJlc937l7ukRHom1mafED1cLjX54Jfy7ih9XZt1nMhC14bFo-xSxQ2RQKf_OGIDK-aESo5ozByKvt_lyiOfl5J0FsVHhlIWmm6CkntEfAqFJK3Lal_-A6yRXFTxRNI4dTokYWourL5gJDv8sdISB94P8tj0cHr6N2
- `reflect.Empty`に関する情報(現在のGoには存在しないことの確認):
    - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGfwL5MyuBi-osjLLYTyaPAsl3CCmezJYvbSaJWQiWJzK-k6WLfvw1c3RxhSGW2Nxpe2M04LxTCJpdNa_tf4kLieMhAUL7g1eDsAsvKz0sl4BQmkyl0hpvq6I-_ptmhQs9aAkBQqlIB-AlJ2TIUf8W6ex7we3bsQmcWYjQwJpj__9JJEe4v1dhw4-pOpaEw0mdiWLdFoINplipZcCHd01yR02PITqk=
    - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHcRx_doOq13cd7rh0Um-FYW8DumtvbBzjWZDKcFg2m58_6fPU9mqp37I2U7g7kB4pkJj9ytwMpuNGef8i991Hc-ZfD-FcY6joRjvAnUnDY5E1T74sY2fw6q2wUoaRcnLqzd0jBRJZO0tLorCrJNJTky9ZEClR0ZA3E16wYbExWTQpTPKHwYcY1rxBTLssLxMq_2Rs9m2QkeS4nEaqc
- Go言語の`fmt`パッケージの進化と`gofmt`ツール:
    - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFkZNCPtT9slIdPuTdb_0KhT8aN_Jj3QsRzH8VMgaxPF-3yh_ZwUqkGOXTZ8cDhdAR7L13vwQSfxp52chpgaF_NAwS0Vv6VgpKSsLd_9zJRRUIBQ9S3B4Ea-J09w3jEZK4XUgTMQS4NpL8Wo2ZUe_aMQss=
    - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHBcUYuFLi9-Fhjqj-vFi3Md0-pN6knXIG_ScYBvjSSveKBv0OW5ttSPDqXIw8BspGazgU3LV3YRmnTqUgq2FYLBLZWKK5zLxfQ9G2r2fN1hPpWs_ZAA1L_uD2xXRYR6JyiFjs=
    - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHz93TEVqr-qkjZR-HvfRTEiioNF0b9pOJf9GdEgBq2de0XvNIK5p3MltXt-99hkWBXPFnf7r5SWwWlVL--k0qghjqbheUl-QK-3PsbOCjPf3KDbp5Os-VMlCKLQOyO2NqGkktUdElV4Nw05CPx4JprsTgsI-pfItLJqoHPI6fBLhDJwKVudx2VtBKUHDsOukueFESJNBxVVNmuOg==