メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

挙動変更

UnixConnのReadFromUnix/ReadFrom/ReadMsgUnixがEOFをnet.OpErrorでラップせず、io.EOFを直接返すようになった。

net

この項目の注釈は AI により生成されており、誤りを含む場合があります。
使用例のコンパイル検証: 検証済み

概要

UnixConnReadFromUnixReadFromReadMsgUnixは、下層の読み取りがEOFに達した場合、これまでio.EOFnet.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/+/754960net: 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.Isio.EOFを判定していたコードは、net.OpErrorUnwrapを実装しているため元々影響を受けない。影響があるのは、err == io.EOFのような直接比較や、EOFを常に*net.OpErrorでラップされるものとして型アサーションしていたコードのみである。

実装解説

UnixConnconnを埋め込んでおり、埋め込み由来のReadは元々err != io.EOFの場合のみ*OpErrorでラップする実装になっていた(修正対象外)。今回修正されたのはUnixConn固有の3メソッドで、いずれも同じパターンでerr != nil && err != io.EOFの条件に変更されている。

関連リンク