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

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

このコミットは、Go言語の標準ライブラリである encoding/csv および encoding/xml パッケージにおいて、書き込みエラーが適切に報告されるように修正を加えるものです。これまでの実装では、内部的な書き込み操作でエラーが発生しても、それが呼び出し元に伝播されないケースがありました。この変更により、これらのパッケージがより堅牢になり、エラーハンドリングが改善されます。

コミット

commit 32a0cbb88187deecfcbcf62f11056ca09fe4a4a0
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Mon Jun 25 16:00:35 2012 -0400

    encoding/csv, encoding/xml: report write errors
    
    Fixes #3773.
    
    R=bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/6327053

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

https://github.com/golang/go/commit/32a0cbb88187deecfcbcf62f11056ca09fe4a4a0

元コミット内容

encoding/csv, encoding/xml: report write errors Fixes #3773. R=bradfitz, rsc CC=golang-dev https://golang.org/cl/6327053

変更の背景

この変更の背景には、Go言語の encoding/csv および encoding/xml パッケージが、データをファイルやネットワークストリームなどの io.Writer インターフェースを通じて書き出す際に、内部で発生した書き込みエラーを適切に呼び出し元に報告していなかったという問題がありました。

具体的には、encoding/csvWriter.WriteAll メソッドでは、複数のレコードを書き込む際に途中でエラーが発生しても、ループを break するだけで、最終的に nil を返していました。これにより、部分的な書き込み成功後にエラーが発生しても、呼び出し元は成功したと誤解する可能性がありました。

encoding/xml パッケージでは、XMLのマーシャリング(Goの構造体をXMLに変換する処理)中に内部の io.Writer への書き込みでエラーが発生した場合、そのエラーが MarshalEncode といった公開APIの戻り値として伝播されないことがありました。これは、特に大きなデータやネットワーク経由での書き込みにおいて、データ損失や不完全な出力の原因となり、デバッグを困難にする可能性がありました。

この問題は、GoのIssue #3773として報告されており、このコミットはその問題を解決するために行われました。エラーを適切に報告することで、アプリケーションは書き込みの失敗を検知し、適切なエラーハンドリングやリカバリロジックを実装できるようになります。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と標準ライブラリの知識が役立ちます。

  • io.Writer インターフェース: Go言語における基本的なI/Oインターフェースの一つで、データを書き込むための抽象化を提供します。Write(p []byte) (n int, err error) メソッドを持ち、n は書き込まれたバイト数、err は書き込み中に発生したエラーを示します。ファイル、ネットワーク接続、バッファなど、様々な出力先がこのインターフェースを実装しています。
  • bufio.Writer: io.Writer をラップし、バッファリング機能を追加する構造体です。これにより、小さな書き込み操作が多数発生しても、実際のシステムコールはまとめて行われるため、I/O性能が向上します。Flush() メソッドを呼び出すことで、バッファに溜まったデータを強制的に基になる io.Writer に書き出します。Flush() は、バッファリング中に発生した書き込みエラーを返すことができます。
  • エラーハンドリング: Go言語では、エラーは多値戻り値の最後の要素として error 型で返されるのが一般的です。呼び出し元は、この error 値をチェックすることで、操作が成功したか失敗したかを判断します。nil はエラーがないことを意味します。
  • encoding/csv パッケージ: CSV (Comma Separated Values) 形式のデータを読み書きするためのパッケージです。csv.Writer は、Goの文字列スライスをCSV形式で io.Writer に書き込む機能を提供します。
  • encoding/xml パッケージ: XML (Extensible Markup Language) 形式のデータをGoの構造体との間でマーシャリング(Goの構造体をXMLに変換)およびアンマーシャリング(XMLをGoの構造体に変換)するためのパッケージです。xml.Encoder は、Goの値をXMLとして io.Writer に書き込む機能を提供します。
  • reflect パッケージ: Goの実行時リフレクション機能を提供します。これにより、プログラムは自身の構造(型、フィールド、メソッドなど)を検査し、動的に操作することができます。encoding/xml パッケージは、Goの構造体をXMLにマーシャリングする際に、リフレクションを使用して構造体のフィールド情報を取得します。

このコミットは、特に bufio.Writer のエラー伝播の仕組みと、Goにおける一般的なエラーハンドリングのプラクティスに焦点を当てています。

技術的詳細

このコミットの技術的な核心は、io.Writer インターフェースを介した書き込み操作で発生するエラーを、より上位のAPI呼び出し元に確実に伝播させる点にあります。

encoding/csv の変更点

encoding/csv/writer.goWriter.WriteAll メソッドは、複数のレコードをCSV形式で書き込むためのものです。変更前は、ループ内で w.Write(record) がエラーを返した場合、break してループを抜けるものの、最終的に return nil を返していました。これは、途中で書き込みエラーが発生しても、WriteAll メソッド自体は成功したかのように振る舞うことを意味します。

変更後は、エラーが発生した場合に即座に return err するように修正されました。さらに重要なのは、ループが正常に終了した後、w.Flush() の結果を返すように変更された点です。csv.Writer は内部で bufio.Writer を使用しており、Flush() はバッファリングされたデータを実際に基になる io.Writer に書き出す際に発生したエラーを報告します。これにより、バッファリングされたデータが書き出される際のエラーも確実に捕捉され、呼び出し元に伝播されるようになりました。

encoding/xml の変更点

encoding/xml パッケージの変更はより広範です。XMLのマーシャリング処理は、内部で printer という構造体を通じて io.Writer への書き込みを行います。

  1. MarshalIndentEncode メソッドの修正: xml.Encoder の公開メソッドである MarshalIndentEncode は、Goの値をXMLに変換して書き出す役割を担います。変更前は、内部の marshalValue がエラーを返しても、enc.Flush() の結果を無視したり、Flush() を呼び出した後に err を返したりしていました。 変更後は、enc.Encode(v) の結果を直接返すか、enc.Flush() の結果を直接返すように修正されました。これにより、マーシャリング中のエラーと、バッファリングされたデータのフラッシュ中のエラーの両方が確実に伝播されるようになりました。

  2. printer 構造体の変更: printer 構造体は、XML要素や属性、テキストコンテンツなどを実際に io.Writer に書き込む低レベルな処理を担当します。

    • marshalValue, marshalSimple, marshalStruct などのメソッドは、以前は書き込み操作後に常に nil を返していました。
    • 変更後は、これらのメソッドが p.cachedWriteError() を呼び出すか、または p.WriteString の戻り値であるエラーを直接返すように修正されました。
  3. cachedWriteError() メソッドの追加: printer 構造体に新しく cachedWriteError() メソッドが追加されました。このメソッドは、内部の bufio.Writer が保持している書き込みエラーを安全に取得するためのものです。bufio.Writer は、書き込み中にエラーが発生した場合、そのエラーを内部にキャッシュし、それ以降の書き込み操作ではそのキャッシュされたエラーを返します。cachedWriteError() は、p.Write(nil) を呼び出すことで、このキャッシュされたエラーを取得します。Write(nil) は何も書き込まずに、内部のエラー状態をチェックする効果があります。

テストの追加

encoding/xml/marshal_test.goTestMarshalWriteErrors という新しいテストが追加されました。このテストは、limitedBytesWriter というカスタムの io.Writer 実装を使用しています。limitedBytesWriter は、指定されたバイト数を超えると書き込みエラーを発生させるように設計されています。このカスタムライターを xml.Encoder に渡し、意図的に書き込みエラーを発生させることで、xml.Encoder がそのエラーを適切に報告するかどうかを検証しています。これにより、エラーハンドリングの修正が正しく機能していることが確認されます。

これらの変更により、encoding/csvencoding/xml パッケージは、基になる io.Writer で発生する書き込みエラーをより正確かつ確実に報告できるようになり、アプリケーションの信頼性が向上しました。

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

src/pkg/encoding/csv/writer.go

--- a/src/pkg/encoding/csv/writer.go
+++ b/src/pkg/encoding/csv/writer.go
@@ -101,11 +101,10 @@ func (w *Writer) WriteAll(records [][]string) (err error) {
 	for _, record := range records {
 		err = w.Write(record)
 		if err != nil {
-			break
+			return err
 		}
 	}
-	w.Flush()
-	return nil
+	return w.w.Flush()
 }
 
 // fieldNeedsQuotes returns true if our field must be enclosed in quotes.

src/pkg/encoding/xml/marshal.go

--- a/src/pkg/encoding/xml/marshal.go
+++ b/src/pkg/encoding/xml/marshal.go
@@ -83,9 +83,7 @@ func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) {
 	enc := NewEncoder(&b)
 	enc.prefix = prefix
 	enc.indent = indent
-	err := enc.marshalValue(reflect.ValueOf(v), nil)
-	enc.Flush()
-	if err != nil {
+	if err := enc.Encode(v); err != nil {
 		return nil, err
 	}
 	return b.Bytes(), nil
@@ -107,8 +105,10 @@ func NewEncoder(w io.Writer) *Encoder {
 // of Go values to XML.
 func (enc *Encoder) Encode(v interface{}) error {
 	err := enc.marshalValue(reflect.ValueOf(v), nil)
-	enc.Flush()
-	return err
+	if err != nil {
+		return err
+	}
+	return enc.Flush()
 }
 
 type printer struct {
@@ -224,7 +224,7 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo) error {\n 	p.WriteString(name)\n 	p.WriteByte('>')\n \n-	return nil
+	return p.cachedWriteError()
 }\n \n var timeType = reflect.TypeOf(time.Time{})\n@@ -260,15 +260,15 @@ func (p *printer) marshalSimple(typ reflect.Type, val reflect.Value) error {\n 	default:\n 		return &UnsupportedTypeError{typ}\n 	}\n-	return nil
+	return p.cachedWriteError()
 }\n \n var ddBytes = []byte("--")\n \n func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error {\n 	if val.Type() == timeType {\n-		p.WriteString(val.Interface().(time.Time).Format(time.RFC3339Nano))\n-		return nil
+		_, err := p.WriteString(val.Interface().(time.Time).Format(time.RFC3339Nano))\n+		return err
 	}\n 	s := parentStack{printer: p}\n 	for i := range tinfo.fields {\
@@ -353,7 +353,13 @@ func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error {\n 		}\n 	}\n 	s.trim(nil)\n-	return nil
+	return p.cachedWriteError()
+}\n+\n+// return the bufio Writer's cached write error
+func (p *printer) cachedWriteError() error {
+	_, err := p.Write(nil)
+	return err
 }\n \n func (p *printer) writeIndent(depthDelta int) {

src/pkg/encoding/xml/marshal_test.go

--- a/src/pkg/encoding/xml/marshal_test.go
+++ b/src/pkg/encoding/xml/marshal_test.go
@@ -5,6 +5,9 @@
 package xml
 
 import (
+	"bytes"
+	"errors"
+	"io"
 	"reflect"
 	"strconv"
 	"strings"
@@ -779,6 +782,55 @@ func TestUnmarshal(t *testing.T) {
 	}\n }\n \n+type limitedBytesWriter struct {\n+\tw      io.Writer\n+\tremain int // until writes fail\n+}\n+\n+func (lw *limitedBytesWriter) Write(p []byte) (n int, err error) {\n+\tif lw.remain <= 0 {\n+\t\tprintln("error")
+\t\treturn 0, errors.New("write limit hit")
+\t}\n+\tif len(p) > lw.remain {\n+\t\tp = p[:lw.remain]\n+\t\tn, _ = lw.w.Write(p)\n+\t\tlw.remain = 0\n+\t\treturn n, errors.New("write limit hit")
+\t}\n+\tn, err = lw.w.Write(p)\n+\tlw.remain -= n\n+\treturn n, err
+}\n+\n+func TestMarshalWriteErrors(t *testing.B) {\n+\tvar buf bytes.Buffer\n+\tconst writeCap = 1024\n+\tw := &limitedBytesWriter{&buf, writeCap}\n+\tenc := NewEncoder(w)\n+\tvar err error\n+\tvar i int\n+\tconst n = 4000\n+\tfor i = 1; i <= n; i++ {\n+\t\terr = enc.Encode(&Passenger{\n+\t\t\tName:   []string{"Alice", "Bob"},\n+\t\t\tWeight: 5,\n+\t\t})\n+\t\tif err != nil {\n+\t\t\tbreak\n+\t\t}\n+\t}\n+\tif err == nil {\n+\t\tt.Error("expected an error")\n+\t}\n+\tif i == n {\n+\t\tt.Errorf("expected to fail before the end")\n+\t}\n+\tif buf.Len() != writeCap {\n+\t\tt.Errorf("buf.Len() = %d; want %d", buf.Len(), writeCap)\n+\t}\n+}\n+\n func BenchmarkMarshal(b *testing.B) {\n \tfor i := 0; i < b.N; i++ {\n \t\tMarshal(atomValue)\n```

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

### `encoding/csv/writer.go` の変更

`WriteAll` メソッドは、複数のレコードを書き込む際に、各レコードの書き込みでエラーが発生した場合に即座にそのエラーを返すように変更されました (`return err`)。これにより、部分的な成功ではなく、最初の失敗で処理を中断し、エラーを呼び出し元に伝播させます。

最も重要な変更は、ループの後に `return w.w.Flush()` が追加されたことです。`csv.Writer` は内部で `bufio.Writer` を使用しており、`w.w` はその `bufio.Writer` を指します。`Flush()` メソッドは、バッファに溜まっているデータを実際に基になる `io.Writer` に書き出します。この `Flush()` がエラーを返す可能性があるため、そのエラーを `WriteAll` の戻り値として返すことで、バッファリングされたデータの書き出し中に発生したエラーも確実に報告されるようになりました。

### `encoding/xml/marshal.go` の変更

1.  **`MarshalIndent` と `Encode` メソッド**:
    これらのメソッドは、XMLエンコーディングの主要なエントリポイントです。変更前は、内部の `marshalValue` がエラーを返しても、その後の `Flush()` の結果を適切に扱っていませんでした。
    変更後は、`enc.Encode(v)` または `enc.Flush()` の結果を直接返すように修正されました。これにより、XML構造の構築中に発生するエラーと、最終的なデータ書き出し(フラッシュ)中に発生するエラーの両方が、呼び出し元に確実に伝播されるようになりました。

2.  **`printer` メソッド群 (`marshalValue`, `marshalSimple`, `marshalStruct`)**:
    これらのメソッドは、XMLの各部分(要素名、属性、テキストコンテンツなど)を実際に `io.Writer` に書き込む役割を担います。変更前は、書き込み操作後に常に `nil` を返していました。
    変更後は、これらのメソッドが `p.cachedWriteError()` を呼び出すか、または `p.WriteString` の戻り値であるエラーを直接返すように修正されました。これは、`bufio.Writer` が書き込みエラーを内部にキャッシュする特性を利用したものです。

3.  **`cachedWriteError()` メソッドの追加**:
    この新しいメソッドは、`printer` 構造体の内部にある `bufio.Writer` が保持しているキャッシュされた書き込みエラーを取得するために導入されました。`bufio.Writer` は、一度書き込みエラーが発生すると、それ以降の書き込み操作ではそのキャッシュされたエラーを返します。`p.Write(nil)` を呼び出すことで、実際にデータを書き込まずに、`bufio.Writer` の内部エラー状態をチェックし、そのエラーを返します。これにより、XMLのマーシャリング処理の途中で発生した低レベルな書き込みエラーが、最終的に `Encode` や `MarshalIndent` の呼び出し元に伝播される経路が確立されました。

### `encoding/xml/marshal_test.go` の変更

`TestMarshalWriteErrors` テストは、`limitedBytesWriter` というカスタム `io.Writer` を定義しています。このカスタムライターは、`remain` フィールドで指定されたバイト数を超えると、`errors.New("write limit hit")` というエラーを返すように実装されています。

この `limitedBytesWriter` を `xml.Encoder` に渡し、`Encode` メソッドを繰り返し呼び出すことで、意図的に書き込みエラーを発生させます。テストは、エラーが期待通りに発生すること (`err == nil` でないこと) と、エラーが発生する前にすべてのデータが書き込まれていないこと (`i == n` でないこと) を検証します。また、バッファに書き込まれたバイト数が `writeCap` (書き込み制限) と一致することも確認し、エラー発生時の状態が正しいことを保証します。

これらの変更とテストの追加により、Goの `encoding/csv` および `encoding/xml` パッケージは、より堅牢なエラーハンドリングを提供し、アプリケーションが書き込みの失敗を確実に検知できるようになりました。

## 関連リンク

*   Go Issue #3773: [https://github.com/golang/go/issues/3773](https://github.com/golang/go/issues/3773)
*   Go CL 6327053: [https://golang.org/cl/6327053](https://golang.org/cl/6327053) (このコミットに対応するGoの変更リスト)

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

*   Go言語の公式ドキュメント:
    *   `io` パッケージ: [https://pkg.go.dev/io](https://pkg.go.dev/io)
    *   `bufio` パッケージ: [https://pkg.go.dev/bufio](https://pkg.go.dev/bufio)
    *   `encoding/csv` パッケージ: [https://pkg.go.dev/encoding/csv](https://pkg.go.dev/encoding/csv)
    *   `encoding/xml` パッケージ: [https://pkg.go.dev/encoding/xml](https://pkg.go.dev/encoding/xml)
*   Go言語のエラーハンドリングに関する一般的な情報源
*   Go言語のテストに関する情報源