挙動変更
UnixConnのReadFromUnix/ReadFrom/ReadMsgUnixがEOFをnet.OpErrorでラップせず、io.EOFを直接返すようになった。
net
この項目の注釈は AI により生成されており、誤りを含む場合があります。
使用例のコンパイル検証: 検証済み
概要
UnixConnのReadFromUnix・ReadFrom・ReadMsgUnixは、下層の読み取りがEOFに達した場合、これまでio.EOFをnet.OpErrorでラップして返すことがあった。今回、これらのメソッドはEOFの場合にio.EOFをそのまま返すよう修正された。
導入経緯
io.Readerのドキュメントは「ReadはラップしたエラーではなくEOF自体を返さなければならない」と規定しており、これはRead系メソッド全般に適用されると解釈されてきた。実際、TCPConn.ReadFromは明示的にio.EOFをラップしない実装になっていたが、UnixConn側の該当メソッドはこの規約に従っておらず、EOFであっても*net.OpErrorでラップして返していた。この不整合がgolang/go#78137で報告され、修正された。
議論のハイライト
- issue本文では、
TCPConn.ReadFromがEOFを明示的にラップしない実装になっている点が、UnixConn側の実装との対比として示された。 - Unixドメインソケットの接続先が先にクローズされた状態で
UnixConn.ReadMsgUnixを呼び出すと、返るエラーがio.EOFそのものではなく*net.OpErrorでラップされたものになる、という再現プログラムが提示された。 - 修正はhttps://go-review.googlesource.com/c/go/+/754960(
net: avoid wrapping io.EOF in UnixConn read methods)として行われた。
使用例
Before
package main
import (
"fmt"
"io"
"net"
"os"
"path/filepath"
)
func main() {
addr := &net.UnixAddr{Name: filepath.Join(os.TempDir(), "unix-eof-example"), Net: "unix"}
ln, err := net.ListenUnix("unix", addr)
if err != nil {
panic(err)
}
defer ln.Close()
go func() {
srv, err := ln.AcceptUnix()
if err != nil {
return
}
// クライアントの読み取りがEOFになるよう、すぐに閉じる。
srv.Close()
}()
cl, err := net.DialUnix("unix", nil, ln.Addr().(*net.UnixAddr))
if err != nil {
panic(err)
}
defer cl.Close()
_, _, _, _, err = cl.ReadMsgUnix(make([]byte, 1), nil)
// これまでは *net.OpError でラップされることがあり、
// io.EOF との直接比較(==)だけでは検出できなかった。
if err == io.EOF {
fmt.Println("got io.EOF directly")
} else if opErr, ok := err.(*net.OpError); ok && opErr.Err == io.EOF {
fmt.Println("got io.EOF wrapped in *net.OpError")
} else {
fmt.Println("other error:", err)
}
}
After
package main
import (
"fmt"
"io"
"net"
"os"
"path/filepath"
)
func main() {
addr := &net.UnixAddr{Name: filepath.Join(os.TempDir(), "unix-eof-example"), Net: "unix"}
ln, err := net.ListenUnix("unix", addr)
if err != nil {
panic(err)
}
defer ln.Close()
go func() {
srv, err := ln.AcceptUnix()
if err != nil {
return
}
srv.Close()
}()
cl, err := net.DialUnix("unix", nil, ln.Addr().(*net.UnixAddr))
if err != nil {
panic(err)
}
defer cl.Close()
_, _, _, _, err = cl.ReadMsgUnix(make([]byte, 1), nil)
// 今後は io.EOF がそのまま返るため、単純な == 比較で検出できる。
if err == io.EOF {
fmt.Println("got io.EOF directly")
} else {
fmt.Println("other error:", err)
}
}
移行時の注意
errors.Isでio.EOFを判定していたコードは、net.OpErrorがUnwrapを実装しているため元々影響を受けない。影響があるのは、err == io.EOFのような直接比較や、EOFを常に*net.OpErrorでラップされるものとして型アサーションしていたコードのみである。
実装解説
UnixConnはconnを埋め込んでおり、埋め込み由来のReadは元々err != io.EOFの場合のみ*OpErrorでラップする実装になっていた(修正対象外)。今回修正されたのはUnixConn固有の3メソッドで、いずれも同じパターンでerr != nil && err != io.EOFの条件に変更されている。
ReadFromUnix(EOF判定は119行目)ReadFrom(EOF判定は131行目)ReadMsgUnix(EOF判定は152行目)