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

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

このコミットは、Go言語の標準ライブラリ io パッケージ内の CopyN 関数の実装を簡素化することを目的としています。具体的には、既存の Copy 関数と LimitReader 関数を組み合わせることで、CopyN の内部ロジックを大幅に削減し、より簡潔で保守性の高いコードに改善しています。

コミット

commit c94eddd1afcd5c63744745605e5a4246273fe3d9
Author: Jeremy Schlatter <jeremy.schlatter@gmail.com>
Date:   Thu Feb 7 20:26:12 2013 -0800

    io: Simplify CopyN implementation by delegating to Copy.
    
    R=golang-dev, dave, minux.ma, bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/7312055

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

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

元コミット内容

io: Simplify CopyN implementation by delegating to Copy.

このコミットメッセージは、「io パッケージの CopyN 関数の実装を、Copy 関数への委譲によって簡素化する」という変更の意図を明確に示しています。

変更の背景

Go言語の io パッケージは、I/O操作のためのプリミティブを提供します。CopyN 関数は、指定されたバイト数だけデータをコピーするための重要な関数です。このコミット以前の CopyN の実装は、内部でバッファを割り当て、ループを使ってデータを読み書きするという、比較的冗長なロジックを持っていました。また、dstio.ReaderFrom インターフェースを実装している場合の最適化も含まれていました。

この変更の背景には、コードの重複を避け、より汎用的な関数(io.Copy)を活用することで、コードベース全体の保守性を向上させるという目的があります。io.Copy は既に効率的なコピーロジックを提供しており、CopyN がその一部を再実装する必要はありません。io.LimitReader を利用することで、CopyN の「Nバイトだけコピーする」という要件を、io.Copy の汎用的なコピー機能に適用できるようになります。これにより、コードがより簡潔になり、潜在的なバグのリスクも低減されます。

前提知識の解説

このコミットを理解するためには、Go言語の io パッケージにおける以下の概念を理解しておく必要があります。

  • io.Reader インターフェース: Read(p []byte) (n int, err error) メソッドを持つインターフェースです。データソースからバイトを読み込む能力を表現します。
  • io.Writer インターフェース: Write(p []byte) (n int, err error) メソッドを持つインターフェースです。データシンクにバイトを書き込む能力を表現します。
  • io.Copy(dst Writer, src Reader) (written int64, err error): src から dst へ、EOF(End Of File)またはエラーが発生するまでデータをコピーする関数です。コピーされたバイト数とエラーを返します。内部で効率的なバッファリングを行ってコピーを実行します。
  • io.CopyN(dst Writer, src Reader, n int64) (written int64, err error): src から dst へ、最大 n バイトのデータをコピーする関数です。n バイトに達する前に src がEOFになった場合、またはエラーが発生した場合は、それまでにコピーされたバイト数とエラーを返します。
  • io.LimitReader(r Reader, n int64) Reader: r から最大 n バイトを読み込む新しい Reader を返します。この Reader は、n バイトを読み込んだ後にEOFを返します。これは、元の Reader の一部だけを読み込みたい場合に非常に便利です。
  • io.ReaderFrom インターフェース: ReadFrom(r Reader) (n int64, err error) メソッドを持つインターフェースです。これは、WriterReader から直接データを読み込むことで、より効率的なコピー操作を実行できる場合に実装されます。例えば、ネットワーク接続やファイルディスクリプタなど、基盤となるシステムコールを直接利用できる場合にパフォーマンス上の利点があります。
  • io.EOF: io パッケージで定義されているエラー変数で、入力の終わりに達したことを示します。
  • io.ErrShortWrite: io パッケージで定義されているエラー変数で、書き込み操作が要求されたバイト数よりも少ないバイトしか書き込めなかった場合に発生します。

技術的詳細

このコミットの核心は、io.CopyN の実装を、より低レベルなループとバッファリングから、高レベルで汎用的な io.Copy 関数への委譲に切り替えることです。

変更前の CopyN の実装: 変更前の CopyN は、以下のロジックを持っていました。

  1. io.ReaderFrom の最適化: dstio.ReaderFrom インターフェースを実装している場合、dst.ReadFrom(io.LimitReader(src, n)) を呼び出して、より効率的なコピーを試みていました。これは、WriterReader から直接データを読み込むことで、中間バッファリングを減らし、システムコールを最適化できる可能性があるためです。
  2. 手動バッファリングとループ: io.ReaderFrom の最適化が適用されない場合、32KB のバッファ (buf := make([]byte, 32*1024)) を作成し、for written < n ループ内で src.Readdst.Write を繰り返し呼び出してデータをコピーしていました。
    • src.Read でデータを読み込み、dst.Write で書き込みます。
    • 読み書きされたバイト数を written に加算します。
    • エラーハンドリング(EOFErrShortWrite、その他のエラー)がループ内で詳細に行われていました。

変更後の CopyN の実装: 変更後の CopyN は、非常に簡潔です。

  1. written, err = Copy(dst, LimitReader(src, n))
    • io.LimitReader(src, n) を使用して、元の src リーダーを、最大 n バイトしか読み込まない新しいリーダーにラップします。
    • この制限されたリーダーを io.Copy 関数に渡します。io.Copy は、この制限されたリーダーから dst へデータをコピーします。io.Copy は、内部で効率的なバッファリングとエラーハンドリングを行います。
  2. if written < n && err == nil { err = EOF }
    • io.Copyn バイト未満しかコピーせず、かつエラーが nil であった場合、それは srcn バイトに達する前にEOFに達したことを意味します。この場合、io.EOF エラーを明示的に設定します。これは CopyN のセマンティクス(n バイトコピーするか、EOFに達するまでコピーする)を維持するために重要です。

この変更により、以下の利点が得られます。

  • コードの簡潔性: 冗長なループ、バッファ管理、詳細なエラーハンドリングが io.Copy に委譲されるため、CopyN のコードが大幅に削減されます。
  • 保守性の向上: CopyN のロジックが簡素化されることで、理解しやすくなり、将来の変更やバグ修正が容易になります。
  • バグのリスク低減: 複雑な手動ループやエラーハンドリングはバグの温床となりがちですが、実績のある io.Copy に任せることで、そのリスクが低減されます。
  • 一貫性: io パッケージ内の関連する関数が、より汎用的なプリミティブを基盤として構築されることで、設計の一貫性が向上します。

特筆すべきは、変更前の io.ReaderFrom による最適化が削除されている点です。これは、io.Copy 関数自体が内部で io.ReaderFromio.WriterTo インターフェースをチェックし、可能な場合は最適化されたパスを使用するため、CopyN で明示的に行う必要がなくなったためです。つまり、io.Copy が既にその最適化を吸収しているため、CopyN はその詳細を知る必要がなくなったのです。

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

src/pkg/io/io.go ファイルの CopyN 関数の実装が変更されています。

--- a/src/pkg/io/io.go
+++ b/src/pkg/io/io.go
@@ -299,43 +299,12 @@ func ReadFull(r Reader, buf []byte) (n int, err error) {
 // If dst implements the ReaderFrom interface,
 // the copy is implemented using it.
 func CopyN(dst Writer, src Reader, n int64) (written int64, err error) {
--	// If the writer has a ReadFrom method, use it to do the copy.
--	// Avoids a buffer allocation and a copy.
--	if rt, ok := dst.(ReaderFrom); ok {
--		written, err = rt.ReadFrom(LimitReader(src, n))
--		if written < n && err == nil {
--			// rt stopped early; must have been EOF.
--			err = EOF
--		}
--		return
--	}
--	buf := make([]byte, 32*1024)
--	for written < n {
--		l := len(buf)
--		if d := n - written; d < int64(l) {
--			l = int(d)
--		}
--		nr, er := src.Read(buf[0:l])
--		if nr > 0 {
--			nw, ew := dst.Write(buf[0:nr])
--			if nw > 0 {
--				written += int64(nw)
--			}
--			if ew != nil {
--				err = ew
--				break
--			}
--			if nr != nw {
--				err = ErrShortWrite
--				break
--			}
--		}
--		if er != nil {
--			err = er
--			break
--		}
--	}
--	return written, err
-+	written, err = Copy(dst, LimitReader(src, n))
-+	if written < n && err == nil {
-+		// src stopped early; must have been EOF.
-+		err = EOF
-+	}
-+	return
  }
  
  // Copy copies from src to dst until either EOF is reached

コアとなるコードの解説

変更前の CopyN (削除されたコード):

	// If the writer has a ReadFrom method, use it to do the copy.
	// Avoids a buffer allocation and a copy.
	if rt, ok := dst.(ReaderFrom); ok {
		written, err = rt.ReadFrom(LimitReader(src, n))
		if written < n && err == nil {
			// rt stopped early; must have been EOF.
			err = EOF
		}
		return
	}
	buf := make([]byte, 32*1024) // 32KBのバッファを確保
	for written < n { // nバイトコピーするまでループ
		l := len(buf)
		if d := n - written; d < int64(l) {
			l = int(d) // 残りバイト数がバッファサイズより小さい場合、バッファサイズを調整
		}
		nr, er := src.Read(buf[0:l]) // srcから読み込み
		if nr > 0 {
			nw, ew := dst.Write(buf[0:nr]) // dstへ書き込み
			if nw > 0 {
				written += int64(nw) // 書き込んだバイト数を加算
			}
			if ew != nil {
				err = ew // 書き込みエラーがあれば記録してループを抜ける
				break
			}
			if nr != nw {
				err = ErrShortWrite // 読み込んだバイト数と書き込んだバイト数が異なる場合、エラー
				break
			}
		}
		if er != nil {
			err = er // 読み込みエラーがあれば記録してループを抜ける
			break
		}
	}
	return written, err

このコードは、まず dstio.ReaderFrom を実装しているかを確認し、実装していればその最適化パスを使用します。そうでなければ、32KBのバッファを確保し、for ループ内で src.Readdst.Write を繰り返し呼び出すことで、手動でデータをコピーしていました。このループ内では、読み込みバイト数と書き込みバイト数の比較、各種エラー(EOF、ShortWriteなど)のハンドリングが詳細に行われていました。

変更後の CopyN (追加されたコード):

	written, err = Copy(dst, LimitReader(src, n))
	if written < n && err == nil {
		// src stopped early; must have been EOF.
		err = EOF
	}
	return

この新しい実装は、わずか数行で同じ機能を実現しています。

  1. LimitReader(src, n): src リーダーを n バイトに制限する新しいリーダーを作成します。この新しいリーダーは、n バイトを読み込んだ時点でEOFを返します。
  2. Copy(dst, LimitReader(src, n)): io.Copy 関数を呼び出し、dst に制限されたリーダーからデータをコピーさせます。io.Copy は内部で効率的なバッファリングとエラーハンドリングを行います。また、io.ReaderFrom などの最適化も io.Copy 自身が処理します。
  3. if written < n && err == nil { err = EOF }: io.Copyn バイト未満しかコピーせず、かつエラーが nil であった場合、それは srcn バイトに達する前にEOFに達したことを意味します。この場合、CopyN のセマンティクスに合わせて io.EOF を返します。

この変更により、CopyN の実装は大幅に簡素化され、より高レベルな抽象化を利用することで、コードの可読性と保守性が向上しました。

関連リンク

参考にした情報源リンク