Fork me on GitHub

io

io パッケージの役割は以下の2つ

  • I/Oプリミティブを抽象化する共通のインターフェース(osのファイルディスクリプタのioとか、バッファのioとか)

  • 関連するI/Oのラッパー

注釈

必ずしもGroutineセーフではない

io.Reader (interface)

シグネチャ

io.Readerは以下のシグネチャを持つReadメソッドを定義しているインターフェースです。p byte[] は引数の読み取りした内容を一時的に格納しておくバッファです。

io.go
type Reader interface {
    Read(p []byte) (n int, err error)
}

ちなみに Read は非常にプリミティブなメソッドなので、通常直接このメソッドを扱うことは少なく、ラッパーの高級関数 (ioutil.ReadAll, ioutil.ReadFile, bufio.Scanner) を使うことが多いのではないでしょうか。

仕様

GoDoc のパッケージコメントから特徴(想定している仕様)を見てみます。

  • 最大 len(p) バイトを読み取り、読み取ったバイト数 n とエラーを返却

  • n < len(p) だったとしてもバッファ p をすべて使っている場合がある

  • 非nilのエラーを返すか、次の呼び出しでエラーを返す(その時はn=0)

  • (0, nil) を返却することは非推奨。(0, nil) はEOFを示さない。終端まで読んだときは (0, EOF) を返す

要は バイト列を読み取るためのインターフェース です。読み取るものは、標準入力でも、ファイルでも、ソケット、バッファでもなんでもよいです。

実装

例えば os.Filebytes.Buffer 構造体は Read(p []byte) (n int, err error) メソッドを実装しており io.Reader インターフェースを満たしています。(同時に io.Writer も満たしていることが多い)

os/file.go
type File struct {
    *file // os specific
}

// ...

// Readメソッドを実装しているので、io.Readerインターフェースを満たしている
// ファイルから len(b) バイトを読み出す
func (f *File) Read(b []byte) (n int, err error) {
    if err := f.checkValid("read"); err != nil {
        return 0, err
    }
    // 実際の読み込み処理は各OSのファイルのreadに移譲
    n, e := f.read(b)
    return n, f.wrapErr("read", e)
}
bytes/buffer.go
type Buffer struct {
    buf      []byte // データ buf[off : len(buf)] などと参照される
    off      int    // オフセット
    lastRead readOp
}

// ...

// バッファから len(p) バイト読み出すか、バッファが空になるまで読む
func (b *Buffer) Read(p []byte) (n int, err error) {
    b.lastRead = opInvalid
    if b.empty() {
        // Buffer is empty, reset to recover space.
        b.Reset()
        if len(p) == 0 {
            return 0, nil
        }
        return 0, io.EOF
    }
    n = copy(p, b.buf[b.off:])
    b.off += n
    if n > 0 {
        b.lastRead = opRead
    }
    return n, nil
}

注釈

ちなみにメソッドのレシーバがポインタ型(*T)だったけど、それで大丈夫?呼び出すときは型 T から呼び出しても大丈夫なの?という疑問があるかも知れません。私はそう思いました。

結論から言うと大丈夫です。というよりもポインタに変換されて呼び出されます。まずGoの仕様として、あるタイプTのポインタ型(*T)として宣言されているメソッドは、レシーバ *T として宣言されたメソッドと T で宣言されたメソッドを含みます。また型 T の変数 t で宣言されたオブジェクトが t.x() のメソッドを呼び出すときに &t のメソッドとして x が含まれている場合は (&t).x() がGoのコンパイラによって変換されて呼び出されます。

https://golang.org/ref/spec#Method_sets https://golang.org/ref/spec#Calls

実際どんな感じで io.ReaderRead メソッドが呼ばれているか ioutil/ioutil.goReadFile メソッドを見てみます。ioutil.ReadFile はファイルからデータを読み取るときに使います。

ioutil/ioutil.go
// ファイルからデータを読み出す
// すべて読んだ場合は EOF error は返さず nil を返す
func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    // ファイルからファイルサイズを取得するが正確でないことがある為
    // 512 バイトを余分に確保しておく。最低 512 バイト確保される
    var n int64 = bytes.MinRead

    if fi, err := f.Stat(); err == nil {
        if size := fi.Size() + bytes.MinRead; size > n {
            n = size
        }
    }
    return readAll(f, n)
}

// ...

// io.Reader から EOF やエラーになるまでデータを読み取る
func readAll(r io.Reader, capacity int64) (b []byte, err error) {
    var buf bytes.Buffer
    // バッファオーバーフローした場合のみpanicをrecoverしてbytes.ErrTooLargeのエラーとして返す
    // それ以外は panic を起こす
    defer func() {
        // 滅多に見ない recover() 関数
        e := recover()
        if e == nil {
            return
        }
        // panicが発生したエラーの型をチェックして、エラー型だった場合は、値を見て
        // bytes.ErrTooLarge("bytes.Buffer: too large")の場合はエラーとして回復
        if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
            err = panicErr
        } else {
            panic(e)
        }
    }()
    if int64(int(capacity)) == capacity {
        buf.Grow(int(capacity))
    }
    // 内部的には bytes の ReadFrom が呼び出される
    _, err = buf.ReadFrom(r)
    return buf.Bytes(), err
}
bytes/buffer.go
// 最小のバッファサイズ(512バイト)
const MinRead = 512

// io.Reader から EOF までデータを読み取り、バッファに追加する
// 必要に応じてバッファを拡張する
// バッファが大きくなりすぎる場合は ErrTooLarge を返す
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
    b.lastRead = opInvalid

    // forループで終了条件 (io.EOF or error) に達するまで処理
    for {
        // *Bufferで保持している内部のバッファを割り当てるだけで十分であれば、拡張したスライスを返す
        // 足りなければ *buffer が保持しているバッファを元の大きさの約2倍に拡張する
        i := b.grow(MinRead)
        b.buf = b.buf[:i]

        // io.Reader を満たしている構造体の Read メソッドを呼び出す
        m, e := r.Read(b.buf[i:cap(b.buf)])
        if m < 0 {
            panic(errNegativeRead)
        }

        b.buf = b.buf[:i+m]
        n += int64(m)
        if e == io.EOF {
            return n, nil // e is EOF, so return nil explicitly
        }
        if e != nil {
            return n, e
        }
    }
}

注釈

os.Openやos.Createで生成したos.File構造体はio.Readerを満たしており、Openした変数をそのままioutil.ReadAllに渡せるので、わざわざioutil.ReadFileがあるのはなんでだろうと思います。ファイルの場合は stat() などによりおおよそのファイルサイズがわかるため、バッファ領域の確保をより正確にするためと想定しています。

インターフェースを扱う

個人的に良い実装だな、と思うのは ReadAll のシグネチャが以下のように io.Reader を受け取るようになっていることです。readAllbytes.ReadFrom, bufio.NewReader も同様。

ReadAll(r io.Reader) ([]byte, error)
readAll(r io.Reader, capacity int64) (b []byte, err error)
ReadFrom(r io.Reader) (n int64, err error)
NewReader(rd io.Reader) *Reader  // bufio.Reader も io.Reader を満たしている

ReadAll メソッドは r io.Reader とインターフェースを引数に取るようになっています。これによって読み出す対象が何であるか気にする必要がなく io.Reader インターフェースを満たす構造体であれば何でも受け取ることできます。ファイルを読みたい場合は ReadFile のようにラッパーとして実装すればよいだけでOKです。

注釈

インターフェースを使った汎用的なデザインになっているのが良いと思っているのでGo特有というわけではない気がします。JavaだとInterfaceとかAbstractクラスとか使って実装する気がします。

上記のメソッド/関数の他にも、例えば json を扱う際の json.NewDecoder は以下のようになっていますし、独自にI/Oを扱う場合は io.Reader を受けとるようにすればよいのではないでしょうか。

func NewDecoder(r io.Reader) *Decoder {
    return &Decoder{r: r}
}

独自の io.Reader を作る

独自の io.Reader インターフェースを実装した myReader 構造体を作ってみます。

type myReader struct {
    content  []byte // 読み出す対象のバイト列
    position int    // 次に読むオフセット
}

func (r *myReader) Read(buf []byte) (int, error) {
    remainingBytes := len(r.content) - r.position
    n := min(remainingBytes, len(buf))
    if n == 0 {
        return 0, io.EOF
    }
    // copyはビルトイン関数
    copy(buf[:n], r.content[r.position:r.position+n])
    r.position += n
    return n, nil
}

func min(a int, b int) int {
    if a < b {
        return a
    }
    return b
}

そうすると以下のように ioutil.ReadAll にわたすことができます。io.Reader インターフェースを満たすだけで、io.Reader を受け取る、ありとあらゆる関数を利用することができます。(以下のサンプル実装の場合はうれしみがないですが)

func main() {
    reader := &myReader{content: []byte("this is the stuff I'm reading")}
    bytes, err := ioutil.ReadAll(reader)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(bytes))
}
// this is the stuff I'm reading

https://play.golang.org/p/xA1UdgJwwdv


注釈

ちなみにファイル終端の EOF は以下のように実装されていました。たしかに error として定義されています。

var EOF = errors.New("EOF")

io.Writer (interface)

シグネチャ

io.Writerio.Reader に似ているインターフェースで以下の Write メソッドだけを持っているインターフェースです。なので、 Write メソッドを満たしていれば io.Writer になれます。

type Writer interface {
    Write(p []byte) (n int, err error)
}

仕様

  • p から len(p) バイトを書き込み、書き込んだバイト数とエラーを返却する

  • n < len(p) の場合は非nilのエラーの返却する必要がある

io.Reader と比較すると仕様がシンプルです。

実装

インターフェースを満たしている構造体の例を見てみます。例えば os.File は以下のように実装しています。

func (f *File) Write(b []byte) (n int, err error) {
    if err := f.checkValid("write"); err != nil {
        return 0, err
    }
    n, e := f.write(b)
    if n < 0 {
        n = 0
    }
    if n != len(b) {
        err = io.ErrShortWrite
    }

    epipecheck(f, e)

    if e != nil {
        err = f.wrapErr("write", e)
    }

    return n, err
}

また bufio.Buffer では以下のように実装しています。

bufio/buffer.go
func (b *Buffer) Write(p []byte) (n int, err error) {
    b.lastRead = opInvalid
    m, ok := b.tryGrowByReslice(len(p))
    if !ok {
        m = b.grow(len(p))
    }
    return copy(b.buf[m:], p), nil
}

実際どんな感じで io.WriterWrite メソッドが呼ばれているか見てみます。

io/io.go
func WriteString(w Writer, s string) (n int, err error) {
    if sw, ok := w.(StringWriter); ok {
        return sw.WriteString(s)
    }
    // 呼び出し元の構造体で実装している Write メソッドを呼び出す
    return w.Write([]byte(s))
}
io/ioutil/ioutil.go
func WriteFile(filename string, data []byte, perm os.FileMode) error {
    f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
    if err != nil {
        return err
    }
    // os.File構造体のWriteを呼び出す
    n, err := f.Write(data)
    if err == nil && n < len(data) {
        err = io.ErrShortWrite
    }
    if err1 := f.Close(); err == nil {
        err = err1
    }
    return err
}

io.Copy (func)

インターフェースではないですが、io.Copy も io パッケージの主要なメソッドの一つだと思うので取り上げます。

仕様

  • src から EOF に到達するかエラーが発生するまで dst にコピー

  • コピーしたバイトするとエラーを返す

実装

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

func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
    if buf != nil && len(buf) == 0 {
        panic("empty buffer in io.CopyBuffer")
    }
    return copyBuffer(dst, src, buf)
}

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 {
        // デフォルトでは 32KB をバッファとして確保
        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
}

注釈

Go Conference で聞いた高度なテクニックですが、sync.Pool でバッファを明示的に指定して io.Copy から io.CopyBuffer にしたところ、メモリ使用量が削減したという話もあります。

https://github.com/src-d/go-git/pull/1179

どちらかというと sync.Pool の性質(メモリに割り当てられている不要なアイテムをキャッシュし、後で再利用することで、 GC の負荷を下げる)を利用しているテクということだと思います。

copyするバイト数がわかっていれば、CopyN で明示的にコピーするバイト数を指定することもできます。io.Copy のラッパー。

func CopyN(dst Writer, src Reader, n int64) (written int64, err error) {
    // LimitReader は src の io.Reader から n バイトだけ読むこむ io.Reader を返却する関数
    written, err = Copy(dst, LimitReader(src, n))
    if written == n {
        return n, nil
    }
    if written < n && err == nil {
        err = EOF
    }
    return
}

io.MultiWriter (func)

io.MultiWriterio.Writer のスライスを内部で保持していて、それぞれの io.WriterWrite メソッドを呼んでいました。デザインパターンでいうところのデコレータパターンらしいです。

io/multi.go
func MultiWriter(writers ...Writer) Writer {
    allWriters := make([]Writer, 0, len(writers))
    for _, w := range writers {
        if mw, ok := w.(*multiWriter); ok {
            allWriters = append(allWriters, mw.writers...)
        } else {
            allWriters = append(allWriters, w)
        }
    }
    return &multiWriter{allWriters}
}

func (t *multiWriter) Write(p []byte) (n int, err error) {
    for _, w := range t.writers {
        n, err = w.Write(p)
        if err != nil {
            return
        }
        if n != len(p) {
            err = ErrShortWrite
            return
        }
    }
    return len(p), nil
}

io.Pipe (func)

インメモリで io.Writer と io.Reader を同期的につなげるパイプの機能。内部でバッファリング されない のが特徴。シーケンシャルに読み書きされる。

io/pipe.go
func Pipe() (*PipeReader, *PipeWriter) {
    p := &pipe{
        wrCh: make(chan []byte),
        rdCh: make(chan int),
        done: make(chan struct{}),
    }
    return &PipeReader{p}, &PipeWriter{p}
}

type pipe struct {
    wrMu sync.Mutex // Serializes Write operations
    wrCh chan []byte
    rdCh chan int

    once sync.Once // Protects closing done
    done chan struct{}
    rerr atomicError
    werr atomicError
}

type multiWriter struct {
    writers []Writer
}

type PipeWriter struct {
    p *pipe
}

// 書き込みの処理は pipe に移譲
func (w *PipeWriter) Write(data []byte) (n int, err error) {
    return w.p.Write(data)
}

func (p *pipe) Write(b []byte) (n int, err error) {
    select {
    // done チャネルがクローズされている場合
    case <-p.done:
        return 0, p.writeCloseError()
    // そうでない場合はmutexでロックを取得
    default:
        p.wrMu.Lock()
        defer p.wrMu.Unlock()
    }

    // このスコープでの once の役割がよくわからない
    for once := true; once || len(b) > 0; once = false {
        select {
        // バイトスライスをチャネルに送信
        case p.wrCh <- b:
            // reader のチャネルから読み込んだint値を受信
            nw := <-p.rdCh
            // reader が読み込んだ文字数バイトスライスをすすめる
            b = b[nw:]
            n += nw
        case <-p.done:
            return n, p.writeCloseError()
        }
    }

    // 読んだ文字数を返却
    // n は名前付き変数の戻り値 (参考: https://golang.org/doc/effective_go.html#named-results)
    return n, nil
}

// クローズした場合はこれが呼ばれる
func (p *pipe) CloseWrite(err error) error {
    if err == nil {
        err = EOF
    }
    p.werr.Store(err)
    // done チャネルのクローズ、sync.Onceなので複数のゴルーチンから同時に呼ばれたとしても一回きりしか呼ばれない
    p.once.Do(func() { close(p.done) })
    return nil
}

その他

上記に上げた io.Readerio.Writer 以外にも io.Closer io.Seeker があります。あとは埋め込みのインターフェースやその他の便利な関数などです。割愛します。