Go言語標準パッケージの深堀調査 - io.Copy()

Go言語標準パッケージへの理解を深めるために、ioパッケージの深堀調査をおこないました。今回はio.Copy関数を調査したので、処理の流れを記事にまとめておきます。

io.Copy()で出来ること

io.Copy関数により、ファイルや標準入力等から読み込んだデータを、標準出力やファイルへ書き出すことが可能です。io.Copy関数は引数としてio.Writerとio.Readerを受け取ります。関数の出力として、コピーされたデータサイズとエラー情報が返されます。

func Copy(dst io.Writer, src io.Reader) (written int64, err error)

io.Copy()のサンプルプログラム

Go言語の公式ドキュメントで紹介されているプログラムを動作確認してみます。このプログラムでは、与えられた文字列をReaderで読み込み、データをos.Stdoutへコピーしています。os.Stdoutは標準出力へデータを書き出すWriterです。

package main

import (
	"io"
	"log"
	"os"
	"strings"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")

	if _, err := io.Copy(os.Stdout, r); err != nil {
		log.Fatal(err)
	}
}
動作結果(標準出力)
some io.Reader stream to be read

io.Copy()の中身

io.Copy関数では、copyBuffer関数が呼ばれていました。この関数はバッファを用いてコピーを行う関数のようです。第三引数でバッファを指定できるのですが、Copy関数ではnilを指定しています。

func Copy(dst Writer, src Reader) (written int64, err error) {
	return copyBuffer(dst, src, nil)
}

さらにcopyBuffer関数の中身を見てみます。

func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
	// If the reader has a WriteTo method, use it to do the copy.
	// Avoids an allocation and a copy.
	if wt, ok := src.(WriterTo); ok {
		return wt.WriteTo(dst)
	}
	// Similarly, if the writer has a ReadFrom method, use it to do the copy.
	if rt, ok := dst.(ReaderFrom); ok {
		return rt.ReadFrom(src)
	}
	if buf == nil {
		size := 32 * 1024
		if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
			if l.N < 1 {
				size = 1
			} else {
				size = int(l.N)
			}
		}
		buf = make([]byte, size)
	}
	for {
		nr, er := src.Read(buf)
		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 {
			if er != EOF {
				err = er
			}
			break
		}
	}
	return written, err
}

序盤のプログラムでは、srcのWriteToメソッド、dstのReadFromメソッドが使えないかチェックしています。もし使える場合、それらのメソッドによってデータのコピーを行っています。
今回のサンプルプログラムでは、src(strings.Reader)にWriteToメソッドが実装されていました。そのため、wt.WriteTo(dst)によってデータコピーが行われました。
後半のプログラムでは、Read・Writeメソッドを使用しつつデータコピーを行っています。src.Read(buf)によりバッファにデータを読み込み、dst.Write(buf[0:nr])によりバッファの中身を読み込んだ分だけ書き出しています。

感想

io.Copy関数のコードを確認することで、データコピーがどのように実行されるのかを整理することができました。io.Writerとio.Readerの実装に関して、パッケージごとに読み書きの処理が異なるので、他のパッケージでの実装も参照していきたいと思います。
今回が初めてのコードリーディングで、やり方が分からず情報整理に苦戦しました。他の方々のブログを参照し、自分に合ったリーディング方法を見つけたいと思います。