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

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

このコミットは、Go言語の標準ライブラリ encoding/csv パッケージにおけるCSVリーダーの挙動変更に関するものです。具体的には、CSVデータの読み込み時に末尾のカンマ(trailing comma)を常に許容するように修正されました。これにより、RFC 4180の解釈を「CSV生成者への勧告」と捉え、リーダー側では末尾カンマをエラーとしない方針に転換しています。

コミット

commit f2bc275525807e1c83f524325d03cd6e7e18fe7d
Author: Pieter Droogendijk <pieter@binky.org.uk>
Date:   Fri Aug 9 15:46:01 2013 +1000

    encoding/csv: always allow trailing commas
    
    Original CL by rsc (11916045):
    
    The motivation for disallowing them was RFC 4180 saying
    "The last field in the record must not be followed by a comma."
    I believe this is an admonition to CSV generators, not readers.
    When reading, anything followed by a comma is not the last field.
    
    Fixes #5892.
    
    R=golang-dev, rsc, r
    CC=golang-dev
    https://golang.org/cl/12294043

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

https://github.com/golang/go/commit/f2bc275525807e1c83f524325d03cd6e7e18fe7d

元コミット内容

このコミットは、encoding/csv パッケージがCSVファイルの読み込み時に末尾のカンマをエラーとして扱っていた挙動を修正するものです。元の実装では、RFC 4180の「レコードの最後のフィールドの後にカンマがあってはならない」という記述を厳密に解釈し、末尾カンマが存在すると ErrTrailingComma エラーを発生させていました。

変更の背景

この変更の背景には、CSVフォーマットに関するRFC 4180の解釈の違いがあります。RFC 4180はCSVの標準を定義していますが、その中の「レコードの最後のフィールドの後にカンマがあってはならない」という記述について、Goの encoding/csv パッケージはこれまで厳密に解釈し、末尾カンマをエラーとしていました。

しかし、このコミットの作者(および元の変更提案者であるrsc)は、この記述はCSVファイルを「生成する側」に対する勧告であり、「読み込む側」に対する制約ではないと解釈しました。つまり、CSVリーダーが末尾カンマに遭遇した場合、それは単にそのフィールドが空であることを意味し、エラーとして扱うべきではないという考えです。

この解釈の変更は、GoのIssue #5892で報告された問題に対応するものです。多くの既存のCSVファイルや他のシステムで生成されたCSVファイルには、末尾カンマが含まれていることがあり、GoのCSVリーダーがこれをエラーとしてしまうと、互換性の問題が生じていました。この変更により、より多くのCSVファイルを柔軟に読み込めるようになり、実用性が向上します。

前提知識の解説

CSV (Comma Separated Values)

CSVは、データをカンマで区切って表現するテキストファイル形式です。表形式のデータを保存する際によく用いられ、異なるアプリケーション間でのデータ交換に広く利用されています。各行が1つのレコードを表し、各レコード内の値がカンマで区切られてフィールドを構成します。

RFC 4180

RFC 4180は、"Common Format and MIME Type for Comma Separated Values (CSV) Files" というタイトルで、CSVファイルの一般的なフォーマットとMIMEタイプを定義したインターネット標準です。このRFCは、CSVファイルの構造、フィールドの引用符(クォート)の扱い、改行コードなどについて規定しています。

このコミットで特に焦点が当てられているのは、RFC 4180のセクション2の「各レコードは、1つ以上のフィールドで構成され、カンマで区切られる。レコードの最後のフィールドの後にカンマがあってはならない。」という記述です。この記述の解釈が、CSVリーダーの実装において重要な論点となります。

Go言語の encoding/csv パッケージ

encoding/csv は、Go言語の標準ライブラリに含まれるパッケージで、CSV形式のデータの読み書きをサポートします。csv.Reader はCSVデータを読み込むための構造体で、Read() メソッドや ReadAll() メソッドを提供します。csv.Writer はCSVデータを書き込むための構造体です。

csv.Reader には、CSVの読み込み挙動を制御するためのいくつかのフィールドがあります。

  • Comma: フィールド区切り文字(デフォルトはカンマ)。
  • Comment: コメント行の開始文字。
  • FieldsPerRecord: 1レコードあたりの期待されるフィールド数。
  • LazyQuotes: 引用符の厳密なチェックを緩和するかどうか。
  • TrimLeadingSpace: フィールドの先頭の空白をトリムするかどうか。
  • TrailingComma: (このコミット以前は)末尾カンマを許容するかどうかを制御するフラグ。

技術的詳細

このコミットの技術的な変更は、主に src/pkg/encoding/csv/reader.gosrc/pkg/encoding/csv/reader_test.go の2つのファイルに集中しています。

src/pkg/encoding/csv/reader.go の変更

  1. ErrTrailingComma の扱い: var ErrTrailingComma = errors.New("extra delimiter at end of line") の行が var ErrTrailingComma = errors.New("extra delimiter at end of line") // no longer used に変更されました。これは、このエラーがコード内で使用されなくなったことを示しています。

  2. Reader 構造体の TrailingComma フィールド: TrailingComma bool // Allow trailing commaTrailingComma bool // ignored; here for backwards compatibility に変更されました。これは、TrailingComma フィールドがもはやCSVリーダーの挙動に影響を与えず、後方互換性のために残されていることを意味します。つまり、このフィールドを true に設定しても、末尾カンマの扱いは変更されません。常に末尾カンマが許容されるようになります。

  3. parseField() メソッドからの末尾カンマチェックロジックの削除: parseField() メソッド内に存在した、!r.TrailingComma の条件に基づいて末尾カンマをチェックし、ErrTrailingComma を返すロジックが完全に削除されました。 削除されたコードブロックは以下の部分です。

    -	if !r.TrailingComma {
    -		// We don't allow trailing commas.  See if we
    -		// are at the end of the line (being mindful
    -		// of trimming spaces).
    -		c := r.column
    -		r1, err = r.readRune()
    -		if r.TrimLeadingSpace {
    -			for r1 != '\n' && unicode.IsSpace(r1) {
    -				r1, err = r.readRune()
    -				if err != nil {
    -					break
    -				}
    -			}
    -		}
    -		if err == io.EOF || r1 == '\n' {
    -			r.column = c // report the comma
    -			return false, 0, r.error(ErrTrailingComma)
    -		}
    -		r.unreadRune()
    -	}
    

    この削除により、CSVリーダーは末尾カンマをエラーとして扱わず、その後に続くフィールドを空の文字列として解釈するようになります。

src/pkg/encoding/csv/reader_test.go の変更

テストファイルでは、末尾カンマを含むCSV入力がエラーとなることを期待していたテストケースが、エラーとならずに空のフィールドとして正しくパースされることを期待するように変更されました。

具体的には、以下のテストケースの名前と期待される出力が変更されています。

  • "BadTrailingCommaEOF" -> "TrailingCommaEOF"
    • 入力: "a,b,c,"
    • 期待出力: [][]string{{"a", "b", "c", ""}} (以前はエラー)
  • "BadTrailingCommaEOL" -> "TrailingCommaEOL"
    • 入力: "a,b,c,\n"
    • 期待出力: [][]string{{"a", "b", "c", ""}} (以前はエラー)
  • "BadTrailingCommaSpaceEOF" -> "TrailingCommaSpaceEOF"
    • 入力: "a,b,c, "
    • 期待出力: [][]string{{"a", "b", "c", ""}} (以前はエラー)
  • "BadTrailingCommaSpaceEOL" -> "TrailingCommaSpaceEOL"
    • 入力: "a,b,c, \n"
    • 期待出力: [][]string{{"a", "b", "c", ""}} (以前はエラー)
  • "BadTrailingCommaLine3" -> "TrailingCommaLine3"
    • 入力: "a,b,c\nd,e,f\ng,hi,"
    • 期待出力: [][]string{{"a", "b", "c"}, {"d", "e", "f"}, {"g", "hi", ""}} (以前はエラー)

また、TrailingComma フィールドがもはや効果を持たないことを示すために、"Issue 2366""Issue 2366a" というテストケースがそれぞれ "TrailingCommaIneffective1""TrailingCommaIneffective2" にリネームされ、TrailingComma: true または false の設定に関わらず、末尾カンマが常に許容されることを確認する内容に変更されています。

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

src/pkg/encoding/csv/reader.go

--- a/src/pkg/encoding/csv/reader.go
+++ b/src/pkg/encoding/csv/reader.go
@@ -72,7 +72,7 @@ func (e *ParseError) Error() string {
 
 // These are the errors that can be returned in ParseError.Error
 var (
-	ErrTrailingComma = errors.New("extra delimiter at end of line")
+	ErrTrailingComma = errors.New("extra delimiter at end of line") // no longer used
 	ErrBareQuote     = errors.New("bare \" in non-quoted-field")
 	ErrQuote         = errors.New("extraneous \" in field")
 	ErrFieldCount    = errors.New("wrong number of fields in line")
@@ -98,16 +98,14 @@ var (
 // If LazyQuotes is true, a quote may appear in an unquoted field and a
 // non-doubled quote may appear in a quoted field.
 //
-// If TrailingComma is true, the last field may be an unquoted empty field.
-//
 // If TrimLeadingSpace is true, leading white space in a field is ignored.
 type Reader struct {
-	Comma            rune // Field delimiter (set to ',' by NewReader)
-	Comment          rune // Comment character for start of line
-	FieldsPerRecord  int  // Number of expected fields per record
-	LazyQuotes       bool // Allow lazy quotes
-	TrailingComma    bool // Allow trailing comma
-	TrimLeadingSpace bool // Trim leading space
+	Comma            rune // field delimiter (set to ',' by NewReader)
+	Comment          rune // comment character for start of line
+	FieldsPerRecord  int  // number of expected fields per record
+	LazyQuotes       bool // allow lazy quotes
+	TrailingComma    bool // ignored; here for backwards compatibility
+	TrimLeadingSpace bool // trim leading space
 	line             int
 	column           int
 	r                *bufio.Reader
@@ -257,23 +255,15 @@ func (r *Reader) parseField() (haveField bool, delim rune, err error) {
 	r.field.Reset()
 
 	r1, err := r.readRune()
-	if err != nil {
-		// If we have EOF and are not at the start of a line
-		// then we return the empty field.  We have already
-		// checked for trailing commas if needed.
-		if err == io.EOF && r.column != 0 {
-			return true, 0, err
-		}
-		return false, 0, err
+	for err == nil && r.TrimLeadingSpace && r1 != '\n' && unicode.IsSpace(r1) {
+		r1, err = r.readRune()
 	}
 
-	if r.TrimLeadingSpace {
-		for r1 != '\n' && unicode.IsSpace(r1) {
-			r1, err = r.readRune()
-			if err != nil {
-				return false, 0, err
-			}
-		}
+	if err == io.EOF && r.column != 0 {
+		return true, 0, err
+	}
+	if err != nil {
+		return false, 0, err
 	}
 
 	switch r1 {
@@ -349,25 +339,5 @@ func (r *Reader) parseField() (haveField bool, delim rune, err error) {
 		return false, 0, err
 	}
 
-	if !r.TrailingComma {
-		// We don't allow trailing commas.  See if we
-		// are at the end of the line (being mindful
-		// of trimming spaces).
-		c := r.column
-		r1, err = r.readRune()
-		if r.TrimLeadingSpace {
-			for r1 != '\n' && unicode.IsSpace(r1) {
-				r1, err = r.readRune()
-				if err != nil {
-					break
-				}
-			}
-		}
-		if err == io.EOF || r1 == '\n' {
-			r.column = c // report the comma
-			return false, 0, r.error(ErrTrailingComma)
-		}
-		r.unreadRune()
-	}
 	return true, r1, nil
 }

src/pkg/encoding/csv/reader_test.go

--- a/src/pkg/encoding/csv/reader_test.go
+++ b/src/pkg/encoding/csv/reader_test.go
@@ -171,32 +171,32 @@ field\"`,\n 		Output: [][]string{{"a", "b", "c"}, {"d", "e"}},\n 	},\n 	{\n-		Name:  "BadTrailingCommaEOF",\n-		Input: "a,b,c,",\n-		Error: "extra delimiter at end of line", Line: 1, Column: 5,\n+		Name:   "TrailingCommaEOF",\n+		Input:  "a,b,c,",\n+		Output: [][]string{{"a", "b", "c", ""}},\n 	},\n 	{\n-		Name:  "BadTrailingCommaEOL",\n-		Input: "a,b,c,\n",\n-		Error: "extra delimiter at end of line", Line: 1, Column: 5,\n+		Name:   "TrailingCommaEOL",\n+		Input:  "a,b,c,\n",\n+		Output: [][]string{{"a", "b", "c", ""}},\n 	},\n 	{\n-		Name:             "BadTrailingCommaSpaceEOF",\n+		Name:             "TrailingCommaSpaceEOF",\n 		TrimLeadingSpace: true,\n 		Input:            "a,b,c, ",\n-		Error:            "extra delimiter at end of line", Line: 1, Column: 5,\n+		Output:           [][]string{{"a", "b", "c", ""}},\n 	},\n 	{\n-		Name:             "BadTrailingCommaSpaceEOL",\n+		Name:             "TrailingCommaSpaceEOL",\n 		TrimLeadingSpace: true,\n 		Input:            "a,b,c, \n",\n-		Error:            "extra delimiter at end of line", Line: 1, Column: 5,\n+		Output:           [][]string{{"a", "b", "c", ""}},\n 	},\n 	{\n-		Name:             "BadTrailingCommaLine3",\n+		Name:             "TrailingCommaLine3",\n 		TrimLeadingSpace: true,\n 		Input:            "a,b,c\nd,e,f\ng,hi,","\n-		Error:            "extra delimiter at end of line", Line: 3, Column: 4,\n+		Output:           [][]string{{"a", "b", "c"}, {"d", "e", "f"}, {"g", "hi", ""}},\n 	},\n 	{\n 		Name:   "NotTrailingComma3",\n@@ -231,7 +231,7 @@ x,,,\n 		},\n 	},\n 	{\n-		Name:             "Issue 2366",\n+		Name:             "TrailingCommaIneffective1",\n 		TrailingComma:    true,\n 		TrimLeadingSpace: true,\n 		Input:            "a,b,\nc,d,e",\n@@ -241,11 +241,14 @@ x,,,\n 		},\n 	},\n 	{\n-		Name:             "Issue 2366a",\n+		Name:             "TrailingCommaIneffective2",\n 		TrailingComma:    false,\n 		TrimLeadingSpace: true,\n 		Input:            "a,b,\nc,d,e",\n-		Error:            "extra delimiter at end of line",\n+		Output: [][]string{\n+			{"a", "b", ""},\n+			{"c", "d", "e"},\n+		},\n 	},\n }\n \n```

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

### `src/pkg/encoding/csv/reader.go`

-   **`ErrTrailingComma` のコメント変更**: `ErrTrailingComma` 変数のコメントが `// no longer used` に変更されました。これは、このエラーがコードパスから削除され、もはや発生しないことを明確に示しています。
-   **`Reader` 構造体の `TrailingComma` フィールドのコメント変更**: `TrailingComma` フィールドのコメントが `// ignored; here for backwards compatibility` に変更されました。これは、このフィールドが以前は末尾カンマの挙動を制御していましたが、このコミット以降は無視され、後方互換性のためだけに存在することを示しています。これにより、ユーザーがこのフィールドをどのように設定しても、末尾カンマは常に許容されるようになります。
-   **`parseField()` メソッドからの末尾カンマチェックロジックの削除**: `parseField()` メソッドは、CSVの個々のフィールドをパースする主要な関数です。この関数から、`!r.TrailingComma` の条件分岐と、それに続く末尾カンマの存在をチェックし、`ErrTrailingComma` を返す一連のロジックが削除されました。この変更が、末尾カンマをエラーとして扱わないという新しい挙動の核心です。これにより、CSVリーダーは末尾カンマを単に空のフィールドの区切りとして解釈し、その後に続くフィールドを空の文字列として結果に含めるようになります。

### `src/pkg/encoding/csv/reader_test.go`

-   **テストケース名の変更と期待出力の修正**: 以前は「BadTrailingComma...」という名前で、末尾カンマがエラーとなることを期待していたテストケースが、「TrailingComma...」という名前に変更され、期待される出力もエラーではなく、末尾に空の文字列 `""` を含むレコードとして修正されました。これは、変更後のCSVリーダーが末尾カンマをエラーとせず、空のフィールドとして正しくパースすることを確認するためのものです。
-   **`TrailingCommaIneffective` テストケースの追加**: `TrailingComma` フィールドがもはや効果を持たないことを確認するために、`TrailingComma: true` と `TrailingComma: false` の両方で同じ入力に対して同じ(末尾カンマが許容される)出力が得られることを検証するテストケースが追加されました。これにより、`TrailingComma` フィールドが後方互換性のためだけに存在し、実際の挙動には影響しないことが保証されます。

これらの変更により、Goの `encoding/csv` パッケージは、RFC 4180の解釈をより柔軟にし、末尾カンマを含むCSVファイルをエラーとせずに読み込めるようになりました。

## 関連リンク

-   Go Issue #5892: [https://github.com/golang/go/issues/5892](https://github.com/golang/go/issues/5892)
-   Go Change List 12294043: [https://golang.org/cl/12294043](https://golang.org/cl/12294043)

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

-   RFC 4180 - Common Format and MIME Type for Comma Separated Values (CSV) Files: [https://datatracker.ietf.org/doc/html/rfc4180](https://datatracker.ietf.org/doc/html/rfc4180)
-   Go言語 `encoding/csv` パッケージのドキュメント: [https://pkg.go.dev/encoding/csv](https://pkg.go.dev/encoding/csv) (このコミットが適用された後のバージョンを参照)
-   Go言語のソースコード (GitHub): [https://github.com/golang/go](https://github.com/golang/go)