net.ConnのReadとWriteの振る舞いについて

Go-MySQL-DriverでカスタムのDialをサポートしていたり圧縮プロトコルサポートのコードをレビューしていたりして、利用している Write() の実装が多様化してきたので、「Write(p)って Read(p)みたいに n が len(p) より小さい場合にループで続きを書き込まなくてい良いのは決まりがあったっけ?」と気になって確認してみました。

まず、 net.Conn の定義ではReadとWriteは次のようになっています。

// https://pkg.go.dev/net#Conn

type Conn interface {
    // Read reads data from the connection.
    // Read can be made to time out and return an error after a fixed
    // time limit; see SetDeadline and SetReadDeadline.
    Read(b []byte) (n int, err error)

    // Write writes data to the connection.
    // Write can be made to time out and return an error after a fixed
    // time limit; see SetDeadline and SetWriteDeadline.
    Write(b []byte) (n int, err error)

ここには n と err の関係どころか n の意味自体書かれていません。実際には net.Conn のReadとWriteは io.Readerio.Writer を満たしているので、そちらのドキュメントを見る必要があります。

https://pkg.go.dev/io#Reader

Reader is the interface that wraps the basic Read method.

Read reads up to len(p) bytes into p. It returns the number of bytes read (0 <= n <= len(p)) and any error encountered. Even if Read returns n < len(p), it may use all of p as scratch space during the call. If some data is available but not len(p) bytes, Read conventionally returns what is available instead of waiting for more.

(略)

https://pkg.go.dev/io#Writer

Writer is the interface that wraps the basic Write method.

Write writes len(p) bytes from p to the underlying data stream. It returns the number of bytes written from p (0 <= n <= len(p)) and any error encountered that caused the write to stop early. Write must return a non-nil error if it returns n < len(p). Write must not modify the slice data, even temporarily.

まとめると次のようになります。

Readの場合

  • 現時点で得られたデータが len(p) よりも少ない場合は、後続のデータを待たずに n < len(p) を返すのが一般的(待っても良い)。
  • n > 0 かつ err != nil の場合があるので、 err をチェックする前に p[:n] までのデータを処理する必要がある。
    • 特に err == io.EOF の場合は正常系として頻出する。
  • Readの実装は、 n にかかわらず、 p 全体を一時メモリとして利用可能。

Writeの場合

  • n < len(p) の場合は errnil でないことが保証されている。
    • つまり、呼び出し側は err == nil の時に改めて p[n:] を書き込む必要はない。
    • そのため、 io.WriteFull() などは存在しない。
  • n > 0 かつ err != nil の場合があるのはReadと同じ。
    • ただしReadと違って正常系の err == io.EOF が無いので、途中までデータが書き込めたことに対して何か処理が必要で無い限りは無視していい。
  • Writeは p に対して一切書き込みを行わない。

ということで、Writeの方が使う側にとってはかなりシンプルですね。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。