[インデックス 14300] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet
パッケージにおけるWindows環境でのエラーメッセージの改善を目的としています。特に、ネットワーク操作が中断された際に、その原因がタイムアウトによるものなのか、それともネットワーク接続が閉じられたことによるものなのかをより正確に区別し、適切なエラーを返すように修正されています。これにより、Windows上でのネットワークアプリケーションのデバッグやエラーハンドリングが容易になります。
コミット
commit 84e20465fc33f1702d6dfc7ed1c05b457acb1b5b
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Fri Nov 2 11:07:22 2012 +1100
net: use better error messages on windows
Fixes #4320.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6810064
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/84e20465fc33f1702d6dfc7ed1c05b457acb1b5b
元コミット内容
このコミットは、net
パッケージのWindows固有のファイルディスクリプタ(fd_windows.go
)において、非同期I/O操作がsyscall.ERROR_OPERATION_ABORTED
エラーで完了した場合の挙動を修正しています。以前は、このエラーが発生すると一律にsyscall.EWOULDBLOCK
(非ブロッキング操作が完了しなかったことを示すエラー)に変換されていました。しかし、このエラーはタイムアウトや接続終了など、複数の原因で発生する可能性があり、一律の変換では正確なエラー情報が失われていました。
このコミットでは、syscall.ERROR_OPERATION_ABORTED
が発生した際に、その原因がタイムアウトによるものか、それとも接続が閉じられたことによるものかを判断し、それぞれerrTimeout
またはerrClosing
というより具体的なエラーを返すように変更されています。また、errClosing
の定義がfd_unix.go
とfd_windows.go
からnet.go
に移動され、net
パッケージ全体で共通のエラーとして利用できるようになりました。
変更の背景
この変更は、Go issue #4320("net: better error messages on windows")に対応するものです。このissueでは、Windows環境でネットワーク接続がタイムアウトした場合や、接続が閉じられた場合に、Goのnet
パッケージが返すエラーメッセージが不明瞭であるという問題が報告されていました。具体的には、これらの状況でsyscall.ERROR_OPERATION_ABORTED
という汎用的なエラーが返され、アプリケーション側でその具体的な原因を特定することが困難でした。
開発者は、より詳細なエラー情報を提供することで、アプリケーションが適切なエラーハンドリングを行い、ユーザーに対してより分かりやすいフィードバックを提供できるようにすることを目的としていました。例えば、タイムアウトと接続切断では、アプリケーションが取るべき対応が異なるため、これらを区別できることは重要です。
前提知識の解説
Go言語のnet
パッケージ
Go言語のnet
パッケージは、ネットワークI/Oのプリミティブを提供します。TCP/IP、UDP、Unixドメインソケットなどのネットワークプロトコルを扱うためのインターフェースが含まれており、クライアントやサーバーアプリケーションを構築するために広く利用されます。このパッケージは、OSのネットワークAPIを抽象化し、クロスプラットフォームで一貫したインターフェースを提供します。
ファイルディスクリプタ(File Descriptor, FD)
ファイルディスクリプタは、Unix系OSにおいてファイルやソケットなどのI/Oリソースを識別するために使用される抽象的なハンドルです。Windowsでは、これに相当する概念として「ハンドル(Handle)」があります。Goのnet
パッケージは、内部的にこれらのOS固有のハンドルを抽象化し、netFD
という構造体で管理しています。fd_unix.go
やfd_windows.go
は、それぞれのOSにおけるnetFD
の実装や、OS固有のI/O操作をラップする役割を担っています。
非同期I/Oと完了ポート(Completion Port)
Windowsでは、高性能な非同期I/Oを実現するためにI/O完了ポート(IOCP: I/O Completion Port)というメカニズムがよく利用されます。これは、複数の非同期I/O操作の完了を効率的に待機するための仕組みです。ioSrv
やExecIO
といった関数は、このIOCPのような非同期I/Oの仕組みをGoのnet
パッケージ内で抽象化し、利用していると考えられます。
syscall.ERROR_OPERATION_ABORTED
これはWindows APIで定義されているエラーコードの一つで、I/O操作が中断されたことを示します。この中断は、様々な原因で発生する可能性があります。例えば、I/O操作がタイムアウトした、関連するハンドルが閉じられた、またはシステムがシャットダウンされた、などです。このエラーコードだけでは、具体的な中断の原因を特定することはできません。
syscall.EWOULDBLOCK
Unix系OSのerrno
(エラー番号)の一つで、非ブロッキングI/O操作が即座に完了しなかったことを示します。Windowsでは、これに相当するエラーコードが存在し、通常はWSAEWOULDBLOCK
として知られています。これは、操作がブロックされることなく実行されたが、データがまだ利用可能でない、または書き込みバッファが満杯である、といった状況で返されます。
errTimeout
とerrClosing
これらはGoのnet
パッケージ内で定義されているカスタムエラーです。
errTimeout
: ネットワーク操作が指定された時間内に完了しなかった場合に返されるエラー。errClosing
: ネットワーク接続が閉じられたために操作が失敗した場合に返されるエラー。
これらのカスタムエラーを導入することで、アプリケーションはより具体的なエラーハンドリングを行うことができます。
技術的詳細
このコミットの核心は、Windows環境における非同期I/Oの完了処理の改善にあります。
src/pkg/net/fd_windows.go
のioSrv.ExecIO
関数は、非同期I/O操作を実行し、その完了を待機する役割を担っています。この関数内で、select
ステートメントを使用して、I/O操作の完了、タイマーの期限切れ、またはファイルディスクリプタのクローズイベントを監視しています。
変更前は、syscall.ERROR_OPERATION_ABORTED
エラーが発生した場合、無条件にsyscall.EWOULDBLOCK
に変換されていました。これは、I/O操作が中断された原因が何であれ、一律に「操作がブロックされなかったが、完了しなかった」という曖昧なエラーを返していたことを意味します。
変更後、timeout
という新しいブール変数が導入されました。select
ステートメント内でタイマーが期限切れになった場合、cancelled
フラグに加えてtimeout
フラグもtrue
に設定されます。
そして、syscall.ERROR_OPERATION_ABORTED
エラーが発生した場合の処理が以下のように変更されました。
if r.err == syscall.ERROR_OPERATION_ABORTED { // IO Canceled
if timeout {
r.err = errTimeout
} else {
r.err = errClosing
}
}
このロジックにより、syscall.ERROR_OPERATION_ABORTED
が発生した際に、timeout
フラグがtrue
であればerrTimeout
を返し、そうでなければerrClosing
を返すようになりました。これにより、I/O操作が中断された具体的な原因(タイムアウトか、接続クローズか)を正確に区別できるようになります。
また、errClosing
の定義がsrc/pkg/net/fd_unix.go
とsrc/pkg/net/fd_windows.go
から削除され、src/pkg/net/net.go
に移動されました。これは、errClosing
が特定のOSの実装に依存するエラーではなく、net
パッケージ全体で共通して使用されるべきエラーであることを示しています。net.go
はnet
パッケージの主要な定義を含むファイルであり、ここに移動することで、net
パッケージの他の部分からもerrClosing
にアクセスしやすくなります。
コアとなるコードの変更箇所
src/pkg/net/fd_unix.go
--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -7,7 +7,6 @@
package net
import (
-"errors"
"io"
"os"
"runtime"
@@ -346,8 +345,6 @@ func (fd *netFD) connect(ra syscall.Sockaddr) error {
return err
}
-var errClosing = errors.New("use of closed network connection")
-
// Add a reference to this fd.
// If closing==true, pollserver must be locked; mark the fd as closing.
// Returns an error if the fd cannot be used.
errors
パッケージのインポートとerrClosing
変数の定義が削除されました。
src/pkg/net/fd_windows.go
--- a/src/pkg/net/fd_windows.go
+++ b/src/pkg/net/fd_windows.go
@@ -196,11 +196,12 @@ func (s *ioSrv) ExecIO(oi anOpIface, deadline int64) (int, error) {
}
// Wait for our request to complete.
var r ioResult
- var cancelled bool
+ var cancelled, timeout bool
select {
case r = <-o.resultc:
case <-timer:
\tcancelled = true
+\t\ttimeout = true
case <-o.fd.closec:
\tcancelled = true
}
@@ -220,7 +221,11 @@ func (s *ioSrv) ExecIO(oi anOpIface, deadline int64) (int, error) {
\t\t// Wait for IO to be canceled or complete successfully.
\t\tr = <-o.resultc
\t\tif r.err == syscall.ERROR_OPERATION_ABORTED { // IO Canceled
-\t\t\tr.err = syscall.EWOULDBLOCK
+\t\t\tif timeout {
+\t\t\t\tr.err = errTimeout
+\t\t\t} else {
+\t\t\t\tr.err = errClosing
+\t\t\t}
\t\t}
}\n \tif r.err != nil {
@@ -312,8 +317,6 @@ func (fd *netFD) connect(ra syscall.Sockaddr) error {
return syscall.Connect(fd.sysfd, ra)\n }\n \n-var errClosing = errors.New("use of closed network connection")\n-\n // Add a reference to this fd.\n // If closing==true, mark the fd as closing.\n // Returns an error if the fd cannot be used.\n```
- `timeout`変数が追加され、タイマー期限切れ時に`timeout = true`が設定されるようになりました。
- `syscall.ERROR_OPERATION_ABORTED`エラーのハンドリングロジックが変更され、`timeout`フラグに基づいて`errTimeout`または`errClosing`を返すようになりました。
- `errClosing`変数の定義が削除されました。
### `src/pkg/net/net.go`
```diff
--- a/src/pkg/net/net.go
+++ b/src/pkg/net/net.go
@@ -221,6 +221,8 @@ func (e *timeoutError) Temporary() bool { return true }\n \n var errTimeout error = &timeoutError{}\n \n+var errClosing = errors.New("use of closed network connection")\n+\n type AddrError struct {\n \tErr string\n \tAddr string\n```
- `errClosing`変数が新しく定義されました。
## コアとなるコードの解説
このコミットの主要な変更は、`src/pkg/net/fd_windows.go`内の`ioSrv.ExecIO`関数に集中しています。
1. **`timeout`変数の導入**:
`var cancelled, timeout bool`
これにより、I/O操作がキャンセルされた理由が、単にキャンセルされたのか(`cancelled`)、それとも特にタイムアウトによってキャンセルされたのか(`timeout`)を区別するためのフラグが追加されました。
2. **タイマー期限切れ時の`timeout`フラグ設定**:
`case <-timer: cancelled = true; timeout = true`
`select`文の中で、I/O操作の完了を待つタイマーが期限切れになった場合、`cancelled`フラグだけでなく、新しく導入された`timeout`フラグも`true`に設定されます。これは、操作がタイムアウトによって中断されたことを明確に示します。
3. **`syscall.ERROR_OPERATION_ABORTED`のより詳細なハンドリング**:
```go
if r.err == syscall.ERROR_OPERATION_ABORTED { // IO Canceled
if timeout {
r.err = errTimeout
} else {
r.err = errClosing
}
}
```
これが最も重要な変更点です。Windowsの非同期I/O操作が`syscall.ERROR_OPERATION_ABORTED`というエラーで完了した場合、このコードブロックが実行されます。
* もし`timeout`フラグが`true`であれば、操作はタイムアウトによって中断されたと判断し、`r.err`を`net`パッケージで定義されている`errTimeout`に設定します。
* `timeout`フラグが`false`であれば、操作はタイムアウト以外の理由(例えば、接続が閉じられたなど)で中断されたと判断し、`r.err`を`net`パッケージで定義されている`errClosing`に設定します。
この変更により、`syscall.ERROR_OPERATION_ABORTED`という汎用的なエラーが、より具体的で意味のある`errTimeout`または`errClosing`に変換されるようになり、アプリケーション側でのエラーハンドリングが格段に向上します。
4. **`errClosing`の定義の移動**:
`src/pkg/net/fd_unix.go`と`src/pkg/net/fd_windows.go`から`var errClosing = errors.New("use of closed network connection")`の定義が削除され、`src/pkg/net/net.go`に移動されました。
これは、`errClosing`が特定のOSの実装に閉じ込められたエラーではなく、`net`パッケージ全体で共通して利用されるべきエラーであることを示しています。`net.go`は`net`パッケージの公開APIや共通の定義を置く場所であるため、ここに移動することで、パッケージ全体での一貫性と再利用性が高まります。
これらの変更により、Windows環境でのネットワークエラーの診断が容易になり、より堅牢なネットワークアプリケーションの開発に貢献します。
## 関連リンク
* Go issue #4320: [https://github.com/golang/go/issues/4320](https://github.com/golang/go/issues/4320)
* Go Change-Id: `6810064` (Gerrit Code Review): [https://golang.org/cl/6810064](https://golang.org/cl/6810064)
## 参考にした情報源リンク
* Go issue #4320の議論内容
* Go言語の`net`パッケージのドキュメント(当時のバージョン)
* Windows APIドキュメント(`ERROR_OPERATION_ABORTED`、I/O完了ポートなど)
* Go言語のソースコード(`src/pkg/net/`ディレクトリ内の関連ファイル)
* Go言語の`syscall`パッケージのドキュメント(当時のバージョン)
* Go言語のエラーハンドリングに関する一般的なプラクティス