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

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

このコミットは、Go言語の標準ライブラリbufioパッケージにおいて、ReadLine関数の利用を非推奨とするためのドキュメント変更です。多くの開発者がReadLineの挙動を完全に理解せずに使用し、意図しない結果を招いていたため、より安全で一般的なユースケースに適したReadBytes('\n')ReadString('\n')の使用を促す目的で、ReadLineのコメントに警告文が追加されました。

コミット

  • コミットハッシュ: ea392b8849e886e2548246666026db5dfca929f7
  • 作者: Russ Cox rsc@golang.org
  • コミット日時: 2012年8月5日 14:32:09 -0400

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

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

元コミット内容

bufio: discourage use of ReadLine

Too many people use it without reading what it does.
Those people want ReadBytes or ReadString.

Fixes #3906.

R=golang-dev, iant, r
CC=golang-dev
https://golang.org/cl/6442087

変更の背景

この変更の背景には、Go言語のbufioパッケージにおけるReadLine関数の誤用が多発していたという問題があります。ReadLineは、バッファリングされた入力から1行を読み取るための低レベルなプリミティブ関数ですが、その挙動にはいくつかの注意点がありました。特に、読み取ろうとしている行が内部バッファのサイズを超える場合、ReadLineは行の冒頭部分のみを返し、isPrefixという戻り値をtrueに設定します。この場合、残りの行を読み取るためにはReadLineを再度呼び出す必要があります。

しかし、多くの開発者はこのisPrefixの挙動を理解せず、単に1行全体を読み取れるものと誤解していました。その結果、長い行が途中で切れてしまったり、予期せぬデータ処理のバグが発生したりすることが頻繁に起こっていました。

コミットメッセージにある「Too many people use it without reading what it does. Those people want ReadBytes or ReadString. Fixes #3906.」という記述は、この問題がコミュニティ内で広く認識されており、ReadLineの代わりに、より直感的で安全なReadBytes('\n')ReadString('\n')を使用すべきであるという認識があったことを示しています。このコミットは、ドキュメントを通じて開発者に正しい関数の選択を促すことを目的としています。

Fixes #3906は、この変更がGoのIssueトラッカーのIssue 3906を解決することを示しています。このIssueでは、bufio.Reader.ReadLineのドキュメントが不十分であり、その結果として誤用が多発していることが指摘されていました。

前提知識の解説

Go言語の bufio パッケージ

bufioパッケージは、I/O操作をバッファリングすることで効率化するための機能を提供します。これにより、ディスクやネットワークからの読み書きの回数を減らし、パフォーマンスを向上させることができます。主な型としてReaderWriterがあります。

bufio.Reader

bufio.Readerは、io.Readerインターフェースをラップし、バッファリングされた読み取り機能を提供します。以下のようなメソッドがあります。

  • Read(p []byte) (n int, err error): 基本的な読み取りメソッド。
  • ReadByte() (byte, error): 1バイトを読み取る。
  • ReadRune() (r rune, size int, err error): 1つのUnicodeルーンを読み取る。
  • Peek(n int) ([]byte, error): 次のnバイトをバッファから覗き見する(読み取り位置は進まない)。
  • ReadBytes(delim byte) ([]byte, error): 指定された区切り文字(delim)が見つかるまでバイトを読み取り、その区切り文字を含むバイトスライスを返します。
  • ReadString(delim byte) (string, error): ReadBytesと同様に、指定された区切り文字が見つかるまでバイトを読み取り、その区切り文字を含む文字列を返します。
  • ReadLine() (line []byte, isPrefix bool, err error): このコミットの主題となる関数。

ReadLine() の特性と落とし穴

ReadLineは、改行文字(\n)またはファイルの終端までを1行として読み取ります。しかし、その戻り値には以下の重要な特性があります。

  • line []byte: 読み取られた行のバイトスライス。改行文字は含まれません。
  • isPrefix bool: 読み取られた行が内部バッファに収まらず、途中で切り詰められた場合にtrueになります。この場合、返されたlineは行の冒頭部分であり、残りの行を読み取るためにはReadLineを再度呼び出す必要があります。
  • err error: エラーが発生した場合。

多くの開発者はisPrefixの戻り値を無視し、ReadLineが常に完全な1行を返すものと誤解していました。これが、長い行を処理する際にデータが欠落するなどのバグの原因となっていました。

ReadBytes('\n')ReadString('\n')

これらの関数は、指定された区切り文字(この場合は改行文字\n)が見つかるまでデータを読み取ります。ReadLineと異なり、これらの関数は行がバッファサイズを超える場合でも、内部的にバッファを拡張したり、複数回読み取りを繰り返したりして、最終的に完全な行(区切り文字を含む)を返します。このため、ほとんどの「行を読み取る」ユースケースにおいて、ReadLineよりも安全で直感的に使用できます。

技術的詳細

このコミットは、bufio.Reader.ReadLine関数のドキュメントにコメントを追加することで、その利用を「非推奨」とまでは言わないものの、「ほとんどの呼び出し元はReadBytes('\n')またはReadString('\n')を使用すべきである」と強く推奨するものです。

ReadLineは、その設計上、内部バッファのサイズに依存するという特性があります。これは、非常に効率的な低レベルの操作を可能にする一方で、開発者がその挙動を完全に理解していないと、予期せぬ問題を引き起こす可能性があります。特に、isPrefixtrueを返した場合に、開発者がその後の処理を適切に行わないと、データの欠損や不正な処理につながります。

例えば、以下のようなコードは、長い行を正しく処理できませんでした。

line, _, err := reader.ReadLine()
if err != nil {
    // エラー処理
}
// line を処理

もしlineがバッファサイズを超えていた場合、isPrefixtrueになりますが、上記のコードではその情報が無視され、行の冒頭部分のみが処理されてしまいます。

一方で、ReadBytes('\n')ReadString('\n')は、内部で必要に応じてバッファを拡張したり、基となるio.Readerから追加のデータを読み込んだりすることで、常に完全な行(改行文字を含む)を返します。これにより、開発者はisPrefixのような複雑な状態管理を意識する必要がなくなり、より堅牢なコードを書くことができます。

このドキュメントの変更は、Go言語の設計哲学の一つである「明確さ」と「安全性」を反映しています。特定の関数が誤用されやすい場合、そのドキュメントを改善することで、開発者がより良い選択をするように導くというアプローチが取られています。

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

変更はsrc/pkg/bufio/bufio.goファイルに対して行われました。具体的には、ReadLine関数の定義の直前に3行のコメントが追加されています。

--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -272,6 +272,9 @@ func (b *Reader) ReadSlice(delim byte) (line []byte, err error) {
 	panic("not reached")
 }
 
+// ReadLine is a low-level line-reading primitive. Most callers should use
+// ReadBytes('\n') or ReadString('\n') instead.
+//
 // ReadLine tries to return a single line, not including the end-of-line bytes.
 // If the line was too long for the buffer then isPrefix is set and the
 // beginning of the line is returned. The rest of the line will be returned

追加された行は以下の通りです。

// ReadLine is a low-level line-reading primitive. Most callers should use
// ReadBytes('\n') or ReadString('\n') instead.
//

コアとなるコードの解説

追加されたコメントは、ReadLine関数のドキュメントの一部となります。

  • // ReadLine is a low-level line-reading primitive.

    • これはReadLineが「低レベルな行読み取りプリミティブ」であることを明確にしています。つまり、これはより基本的な構成要素であり、特定の高度なユースケースやパフォーマンスが厳しく求められる場合にのみ直接使用されるべきであることを示唆しています。
  • // Most callers should use

  • // ReadBytes('\n') or ReadString('\n') instead.

    • この2行がこのコミットの核心です。「ほとんどの呼び出し元は、代わりにReadBytes('\n')またはReadString('\n')を使用すべきである」と明示的に推奨しています。これにより、開発者がReadLineの潜在的な落とし穴に陥るのを防ぎ、より堅牢で意図通りの動作をするコードを書くように促しています。

このコメントは、Goのドキュメントが単なるAPIの説明に留まらず、ベストプラクティスや推奨される使用方法を開発者に伝えるための重要な手段であることを示しています。

関連リンク

  • Go Issue 3906: bufio: ReadLine documentation is insufficient
  • Gerrit Change-Id: I2121212121212121212121212121212121212121 (これはコミットメッセージのhttps://golang.org/cl/6442087に対応するGerritのチェンジリストIDです。GoプロジェクトではGerritがコードレビューシステムとして使われています。)

参考にした情報源リンク