GoでTCPソケットを読み書きするときに起こるエラー

ソケット通信は双方向で遠隔という複雑な条件下のためさまざまなエラーが発生する。 Goでソケット通信を書いていて、言語の力のおかげで記述は楽になっているが、下層で同じOSの機能を使っている以上エラーは避けられない。

そもそも、どんなときにどんなエラーが起こりうるのか、特にGoの net パッケージではどのようにエラーを扱っているのか、今まであまり考えずに正常系のみを実装していたので これはよくないなって思って調べた。

ソケット通信の基本 read と write

TCPにおけるソケット通信は基本 read と write である。名前の通り、読み・書きを表す。 ソケット通信でエラーが起きるのは、この read/write の処理の中が多い。

net.OpError によるラップ

Goでソケット通信中に起こるエラーはほぼすべて net.OpError 構造体でラップされる。 ただし io.EOF のようにラップされないものもある。

net.conn.Read のコードを見てみると、io.EOF を除くエラーはすべて net.OpError でラップされていた。

// Read implements the Conn Read method.
func (c *conn) Read(b []byte) (int, error) {
    if !c.ok() {
        return 0, syscall.EINVAL
    }
    n, err := c.fd.Read(b)
    if err != nil && err != io.EOF {
        err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
    }
    return n, err
}

よって、net.OpError でラップされた中身の元のエラーを含めて判定するにはGo1.13~の errors パッケージを使って次のようにするとよさそう

if errors.Is(err, syscall.EPIPE) {
    ...
}

発生しうるエラーたち

実際に read/write 中に片方を切断して、試した。 なるほど、read と write では返ってくるエラーが違うのか…今までそれすら知らなかった。

エラーの実体 エラー文字列 どんなときに起こるか
io.EOF "EOF" 相手が切断したソケットに対して read を呼び出すと発生
syscall.EPIPE "broken pipe" 相手が切断したソケットに対して write を呼び出すと発生
(正確には1回目の write は成功するが、次の write 呼び出しで EPIPE が発生する)
poll.ErrNetClosing(※) "use of closed network connection" 既にCloseしたソケットに対してread/write を呼び出すと発生

※…poll.ErrNetClosinginternal/pollパッケージの中に入っているため、外部から参照することはできない。

コメントには次のように書いてあった。

ErrNetClosing is returned when a network descriptor is used after it has been closed. Keep this string consistent because of issue #4373: since historically programs have not been able to detect this error, they look for the string.

これはつまり、ErrNetClosing かどうかを判断したい場合は文字列 "use of closed network connection" という文字列と比較するしかないのか…うーーむ。。

他にもエラーはある

syscall.EPIPE もあるように、syscall. から始まる系はOSのエラーコードと一致している。 よって、Goのレイヤーで見なくても、例えばMan page of sendを見ると E から始まるエラー定数がたくさん列挙されている。実際にこの他のエラーを発生させる実験はしなかったが、おそらくGoでも syscall.Eなんとか で判定できるはず。