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

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

このコミットは、Go言語の標準ライブラリである fmt パッケージにおけるスキャン処理の改善に関するものです。具体的には、入力ストリームから空白文字を読み飛ばす際に、Windows環境でよく見られる改行コードである \r\n (キャリッジリターンとラインフィードの組み合わせ) を、単一の \n (ラインフィード) と同等に扱うように変更しています。これにより、クロスプラットフォームでの fmt.Scan 系の関数の挙動の一貫性が向上し、特にWindows環境での入力処理の互換性が改善されます。

コミット

commit 221af5c12fe9769b723b8af2f000ed5f39a5dbb3
Author: Rob Pike <r@golang.org>
Date:   Wed Jul 31 15:00:08 2013 +1000

    fmt: treat \r\n as \n in Scan
    When scanning input and "white space" is permitted, a carriage return
    followed immediately by a newline (\r\n) is treated exactly the same
    as a plain newline (\n). I hope this makes it work better on Windows.
    
    We do it everywhere, not just on Windows, since why not?
    
    Fixes #5391.
    
    R=golang-dev, dsymonds
    CC=golang-dev
    https://golang.org/cl/12142043

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

https://github.com/golang/go/commit/221af5c12fe9769b723b8af2f000ed5f39a5dbb3

元コミット内容

fmt: treat \r\n as \n in Scan

このコミットは、fmt パッケージのScan系の関数が入力から空白文字を読み飛ばす際に、\r\n (キャリッジリターンとラインフィード) のシーケンスを、単一の \n (ラインフィード) と同じように扱うように変更します。この変更は、特にWindows環境での動作改善を目的としていますが、特定のプラットフォームに限定せず、全ての環境でこの挙動を適用します。この変更は、Issue #5391 を修正するものです。

変更の背景

Go言語の fmt パッケージは、C言語の scanf に似た機能を提供し、書式化された入力の読み込みを扱います。テキストファイルを扱う際、異なるオペレーティングシステム間では改行コードの表現が異なります。

  • Unix/Linux/macOS: 主に \n (ラインフィード、LF) を改行コードとして使用します。
  • Windows: 主に \r\n (キャリッジリターンとラインフィード、CRLF) を改行コードとして使用します。

fmt パッケージの Scan 系の関数(例: fmt.Scan, fmt.Fscan, fmt.Sscan など)は、デフォルトで空白文字(スペース、タブ、改行など)を区切り文字として扱います。しかし、これまでの実装では、\r\n のシーケンスが \r\n の2つの異なる空白文字として解釈される可能性がありました。

この挙動は、特にWindowsで作成されたテキストファイルをGoプログラムで読み込む際に問題を引き起こす可能性がありました。例えば、数値の後に \r\n が続く場合、\r が予期せぬ空白文字として扱われ、次のスキャン操作に影響を与えることが考えられます。

このコミットは、このプラットフォーム間の改行コードの違いに起因する問題を解決し、fmt パッケージのスキャン処理がより堅牢でクロスプラットフォーム互換性を持つようにすることを目的としています。Issue #5391 は、この問題がユーザーによって報告されたことを示唆しています。

前提知識の解説

1. fmt パッケージとスキャン関数

Go言語の fmt パッケージは、書式化されたI/O(入出力)を扱うための機能を提供します。fmt.Scan, fmt.Fscan, fmt.Sscan などの関数は、それぞれ標準入力、io.Reader、文字列から書式化されたデータを読み込むために使用されます。これらの関数は、指定されたフォーマットに従って入力ストリームから値を抽出し、対応する変数に格納します。

スキャン関数は、デフォルトで空白文字(スペース、タブ、改行)を区切り文字として扱います。これは、例えば "123 456" のような入力から 123456 を別々の数値として読み込む際に便利です。

2. 改行コード (\n\r\n)

  • \n (LF - Line Feed): ラインフィード。カーソルを次の行の同じ桁に移動させます。Unix系システムで一般的な改行コードです。ASCIIコードは10 (0x0A)。
  • \r (CR - Carriage Return): キャリッジリターン。カーソルを行の先頭に移動させます。タイプライターのキャリッジ(印字ヘッド)を戻す動作に由来します。ASCIIコードは13 (0x0D)。
  • \r\n (CRLF): キャリッジリターンとラインフィードの組み合わせ。カーソルを行の先頭に移動させ、さらに次の行に移動させます。Windowsシステムや多くのインターネットプロトコル(HTTPなど)で一般的な改行コードです。

歴史的に、タイプライターの動作を模倣するために \r\n が使われていましたが、Unix系システムでは \n だけで改行を表すようになりました。この違いが、異なるOS間でテキストファイルを交換する際に互換性の問題を引き起こすことがあります。

3. io.Readerpeek メソッド

Go言語では、入力ストリームは io.Reader インターフェースを通じて抽象化されます。fmt パッケージのスキャン処理の内部では、この io.Reader からバイトを読み取ります。

コミットで変更されている skipSpace 関数は、内部的に s.peek("\n") のような操作を行っています。これは、ストリームから実際に読み込むことなく、次に続く文字が何かを「覗き見」する操作です。これにより、\r を読み込んだ後に、その次に \n が続くかどうかを効率的に判断し、\r\n シーケンス全体を単一の改行として処理することが可能になります。

技術的詳細

このコミットの技術的な核心は、fmt パッケージの内部で空白文字をスキップするロジック、特に skipSpace 関数にあります。

skipSpace 関数は、入力ストリームから空白文字を読み飛ばす役割を担っています。これまでの実装では、\r\n はそれぞれ独立した空白文字として扱われる可能性がありました。

変更後の skipSpace 関数では、以下のロジックが追加されました。

		if r == '\r' && s.peek("\n") {
			continue
		}

このコードは、以下の挙動を実現します。

  1. r == '\r':現在読み込んだ文字がキャリッジリターン (\r) であるかをチェックします。
  2. s.peek("\n"):もし現在文字が \r であった場合、次に続く文字がラインフィード (\n) であるかを「覗き見」します。peek メソッドは、実際にストリームから文字を消費することなく、次の文字を調べることができます。
  3. continue:もし \r の直後に \n が続くことが確認された場合、現在の \r はスキップされ、ループの次のイテレーションに進みます。これにより、\r は空白文字として処理されず、その後の \n が通常の改行として処理されることになります。結果として、\r\n のシーケンス全体が単一の改行として扱われるようになります。

この変更により、fmt パッケージのスキャン関数は、Windowsスタイルの改行コード \r\n を、Unixスタイルの改行コード \n と同じように、単一の改行として認識するようになります。これにより、異なるOSで作成されたテキストファイルをGoプログラムでスキャンする際の互換性と堅牢性が向上します。

また、この変更は fmt/doc.go にも反映され、この新しい挙動が公式ドキュメントに明記されることで、ユーザーがこの動作を理解しやすくなっています。

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

このコミットにおけるコアとなるコードの変更は、主に以下の3つのファイルにわたります。

  1. src/pkg/fmt/doc.go: fmt パッケージのドキュメントに、\r\n の扱いに関する説明が追加されました。

    --- a/src/pkg/fmt/doc.go
    +++ b/src/pkg/fmt/doc.go
    @@ -215,6 +215,10 @@
     	stops if it does not, with the return value of the function
     	indicating the number of arguments scanned.
     
    +	In all the scanning functions, a carriage return followed
    +	immediately by a newline is treated as a plain newline
    +	(\r\n means the same as \n).
    +
     	In all the scanning functions, if an operand implements method
     	Scan (that is, it implements the Scanner interface) that
     	method will be used to scan the text for that operand.  Also,
    
  2. src/pkg/fmt/scan.go: スキャン処理の内部で空白文字をスキップする skipSpace 関数にロジックが追加されました。

    --- a/src/pkg/fmt/scan.go
    +++ b/src/pkg/fmt/scan.go
    @@ -437,6 +437,9 @@ func (s *ss) skipSpace(stopAtNewline bool) {
     		if r == eof {
     			return
     		}
    +		if r == '\r' && s.peek("\n") {
    +			continue
    +		}
     		if r == '\n' {
     			if stopAtNewline {
     				break
    
  3. src/pkg/fmt/scan_test.go: \r\n の扱いを検証するための新しいテストケースが追加されました。

    --- a/src/pkg/fmt/scan_test.go
    +++ b/src/pkg/fmt/scan_test.go
    @@ -192,6 +192,10 @@ var scanTests = []ScanTest{\n \t{\"-.45e1-1e2i\\n\", &complex128Val, complex128(-.45e1 - 100i)},\n \t{\"hello\\n\", &stringVal, \"hello\"},\n \n    +// Carriage-return followed by newline. (We treat \r\n as \n always.)\n    +\t{\"hello\\r\\n\", &stringVal, \"hello\"},\n    +\t{\"27\\r\\n\", &uint8Val, uint8(27)},\n    +\n     \t// Renamed types\n     \t{\"true\\n\", &renamedBoolVal, renamedBool(true)},\n     \t{\"F\\n\", &renamedBoolVal, renamedBool(false)},\
    

コアとなるコードの解説

src/pkg/fmt/doc.go の変更

この変更は、fmt パッケージのドキュメントに、Scan 関数が \r\n\n と同じように扱うという新しい挙動を明記するものです。これにより、ユーザーは fmt パッケージの入力処理がプラットフォーム間の改行コードの違いを吸収することを知ることができます。これは、APIの挙動を明確にし、予期せぬ動作を防ぐ上で非常に重要です。

src/pkg/fmt/scan.goskipSpace 関数の変更

skipSpace 関数は、fmt パッケージの内部で、入力ストリームから空白文字(スペース、タブ、改行など)を読み飛ばすために使用されます。この関数は、fmt.Scan などの関数が数値や文字列などの実際のデータを読み込む前に、不要な空白をスキップする役割を担っています。

追加された以下のコードブロックが、このコミットの主要な機能変更点です。

		if r == '\r' && s.peek("\n") {
			continue
		}
  • r == '\r':これは、現在読み込んだ文字 r がキャリッジリターン (\r) であるかどうかをチェックします。
  • s.peek("\n"):これは、s (スキャナーの状態を表す構造体) の peek メソッドを呼び出しています。peek メソッドは、実際にストリームから文字を消費することなく、次に続く文字が引数で指定された文字列(この場合は "\n")と一致するかどうかを調べます。
  • continue:もし現在の文字が \r であり、かつその直後に \n が続く場合、continue ステートメントが実行されます。これは、現在の \r をスキップし、skipSpace 関数のループの次のイテレーションに進むことを意味します。

このロジックにより、\r\n のシーケンスが検出された場合、\r は無視され、その後の \n が通常の改行として処理されます。結果として、\r\n は単一の改行として扱われ、Windows環境で作成されたファイルからの入力も、Unix環境と同様にスムーズに処理されるようになります。

src/pkg/fmt/scan_test.go の変更

テストケースの追加は、この変更が意図した通りに機能することを保証するために不可欠です。

	// Carriage-return followed by newline. (We treat \r\n as \n always.)
	{"hello\r\n", &stringVal, "hello"},
	{"27\r\n", &uint8Val, uint8(27)},

これらのテストケースは、fmt.Scan\r\n で終わる文字列を正しくスキャンできることを検証しています。

  • "hello\r\n" の場合、stringVal"hello" が正しく格納されることを確認します。
  • "27\r\n" の場合、uint8Val27 が正しく格納されることを確認します。

これらのテストは、\r\n が単一の改行として扱われ、スキャン処理がそこで終了し、余分な文字が残らないことを保証します。

関連リンク

参考にした情報源リンク