[インデックス 13173] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/rpc
パッケージ内の client.go
ファイルに対する変更です。具体的には、RPCクライアントがサーバーからの応答を読み取るロジックが改善されています。
コミット
dcc80e4553e4a9a9676d0fd35092cc1009bc148c: net/rpc: improve response reading logic
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dcc80e4553e4a9a9676d0fd35092cc1009bc148c
元コミット内容
net/rpc: improve response reading logic
CL 5956051 introduced too many call != nil checks, so
attempt to improve this by splitting logic into three
distinct parts.
R=r
CC=golang-dev
https://golang.org/cl/6248048
変更の背景
このコミットの背景には、以前の変更である CL 5956051
があります。CL 5956051
は、net/rpc
クライアントの応答読み取りロジックに特定の変更をもたらしましたが、その結果として call != nil
というnilチェックが過剰に発生するようになりました。これはコードの可読性や保守性を低下させる可能性がありました。
このコミットは、その過剰なnilチェックを解消し、応答処理ロジックをより明確で構造化された形に改善することを目的としています。具体的には、応答の読み取りと処理を3つの異なる論理パスに分割することで、コードの意図を明確にし、冗長な条件分岐を削減しています。これにより、RPCクライアントの応答処理がより堅牢で理解しやすくなることが期待されます。
前提知識の解説
Go言語の net/rpc
パッケージ
net/rpc
はGo言語の標準ライブラリで、Goプログラム間でリモートプロシージャコール (RPC) を行うための機能を提供します。RPCは、別のプロセス空間(通常は別のマシン上)にあるプロシージャ(関数やメソッド)を、あたかもローカルにあるかのように呼び出すための技術です。
net/rpc
パッケージは、クライアントとサーバーの両方の実装を提供します。
- サーバー: サービスを登録し、クライアントからのRPCリクエストを待ち受け、処理します。
- クライアント: サーバーに接続し、リモートのプロシージャを呼び出します。
RPCの基本的な流れは以下の通りです。
- クライアントが呼び出し: クライアントはリモートの関数を呼び出します。
- 引数のマーシャリング: クライアント側で引数がネットワーク経由で送信可能な形式(例: JSON、Gob)にシリアライズ(マーシャリング)されます。
- ネットワーク送信: シリアライズされた引数がネットワーク経由でサーバーに送信されます。
- 引数のアンマーシャリング: サーバー側で引数がデシリアライズ(アンマーシャリング)されます。
- サーバーでの実行: サーバーは指定された関数を実行します。
- 結果のマーシャリング: サーバー側で結果がシリアライズされます。
- ネットワーク送信: シリアライズされた結果がネットワーク経由でクライアントに送信されます。
- 結果のアンマーシャリング: クライアント側で結果がデシリアライズされます。
- クライアントでの受け取り: クライアントは結果を受け取ります。
client.go
と input()
メソッド
net/rpc
パッケージの client.go
ファイルは、RPCクライアントの実装を含んでいます。このファイル内の input()
メソッドは、RPCサーバーからの応答を非同期的に読み取り、処理する役割を担っています。通常、このメソッドはゴルーチンとして実行され、サーバーからの応答ストリームを継続的に監視します。
input()
メソッドの主な責務は以下の通りです。
- サーバーからの応答ヘッダ(
response
)を読み取る。 - 応答ヘッダに含まれるシーケンス番号(
seq
)に基づいて、対応する保留中のRPC呼び出し(call
)を特定する。 - 応答がエラーであるか、正常な結果であるかに応じて、適切な処理を行う。
- 応答ボディを読み取り、
call.Reply
にデコードするか、エラーボディとして破棄する。 - RPC呼び出しが完了したことを通知する(
call.done()
)。
Go言語の switch
ステートメント
Go言語の switch
ステートメントは、複数の条件分岐を簡潔に記述するための制御構造です。他の言語の switch
とは異なり、Goの switch
は暗黙的な fallthrough
がなく、各 case
は自動的に break
します(明示的に fallthrough
キーワードを使用しない限り)。
このコミットでは、switch
ステートメントが条件式なしで使用されています。これは、複数のブール条件を評価し、最初に真になった case
ブロックを実行する「タグなしswitch」として機能します。これにより、一連の if-else if-else
構造をより読みやすく、構造化された形で表現できます。
技術的詳細
このコミットの主要な変更は、client.go
の input()
メソッド内における応答処理ロジックの再構築です。以前は複数の if-else if
と call != nil
チェックが散在していましたが、これを単一の switch
ステートメントに集約し、3つの明確なケースに分割しています。
変更前のコードは、主に以下の2つの大きな if
ブロックで構成されていました。
call == nil || response.Error != ""
の場合(エラー応答または対応する呼び出しがない場合)response.Error == ""
の場合(正常応答の場合)
これらのブロック内でさらに call != nil
のチェックが行われており、ロジックが複雑になっていました。
変更後のコードでは、以下の3つの case
を持つ switch
ステートメントが導入されています。
-
case call == nil:
- 意味: サーバーからの応答に対応する保留中のRPC呼び出し (
call
) が見つからなかった場合。これは通常、クライアントがリクエストを送信する際に部分的に失敗し、call
が既に削除されている状況で、サーバーがリクエストボディの読み取りエラーについて応答を返してきた場合に発生します。 - 処理: この場合、応答ボディは破棄されます (
client.codec.ReadResponseBody(nil)
)。エラーが発生した場合は、そのエラーが記録されますが、call
が存在しないため、特定の呼び出しにエラーを割り当てることはできません。 - 改善点: 以前は
if call == nil || response.Error != ""
の一部として処理され、その後のif call != nil
との組み合わせで混乱を招く可能性がありました。このケースを独立させることで、この特定のシナリオの意図が明確になります。
- 意味: サーバーからの応答に対応する保留中のRPC呼び出し (
-
case response.Error != "":
- 意味: サーバーからエラー応答が返された場合。
call
は存在しますが、サーバー側で処理中にエラーが発生したことを示します。 - 処理:
call.Error
にServerError(response.Error)
を設定し、サーバーからのエラーメッセージをRPC呼び出しに伝播します。その後、応答ボディは破棄されます (client.codec.ReadResponseBody(nil)
)。応答ボディの読み取り中にエラーが発生した場合も、そのエラーが記録されます。最後にcall.done()
を呼び出して、このRPC呼び出しが完了したことを通知します。 - 改善点: 以前は
if call == nil || response.Error != ""
の一部として処理され、if call != nil
の内部でエラーが設定されていました。このケースを独立させることで、エラー応答の処理パスが明確になり、call != nil
の冗長なチェックが不要になります。
- 意味: サーバーからエラー応答が返された場合。
-
default:
- 意味: 上記のどのケースにも当てはまらない場合。これは、サーバーから正常な応答が返され、対応する
call
が存在する場合を意味します。 - 処理:
client.codec.ReadResponseBody(call.Reply)
を呼び出して、応答ボディをcall.Reply
にデコードします。応答ボディの読み取り中にエラーが発生した場合、call.Error
にそのエラーを設定します。最後にcall.done()
を呼び出して、このRPC呼び出しが完了したことを通知します。 - 改善点: 以前は
else if response.Error == ""
のブロックとして処理されていました。default
ケースとして扱うことで、正常系の処理が明確に区別され、コードのフローがより直感的になります。
- 意味: 上記のどのケースにも当てはまらない場合。これは、サーバーから正常な応答が返され、対応する
この変更により、call != nil
のチェックが大幅に削減され、各応答シナリオ(対応する呼び出しがない、エラー応答、正常応答)が独立した case
として扱われるため、コードの論理的な分離が促進され、可読性と保守性が向上しています。
コアとなるコードの変更箇所
--- a/src/pkg/net/rpc/client.go
+++ b/src/pkg/net/rpc/client.go
@@ -116,24 +116,32 @@ func (client *Client) input() {
delete(client.pending, seq)
client.mutex.Unlock()
- if call == nil || response.Error != "" {
+ switch {
+ case call == nil:
+ // We've got no pending call. That usually means that
+ // WriteRequest partially failed, and call was already
+ // removed; response is a server telling us about an
+ // error reading request body. We should still attempt
+ // to read error body, but there's no one to give it to.
+ err = client.codec.ReadResponseBody(nil)
+ if err != nil {
+ err = errors.New("reading error body: " + err.Error())
+ }
+ case response.Error != "":
// We've got an error response. Give this to the request;
// any subsequent requests will get the ReadResponseBody
// error if there is one.
- if call != nil {
- call.Error = ServerError(response.Error)
- }
+ call.Error = ServerError(response.Error)
err = client.codec.ReadResponseBody(nil)
if err != nil {
err = errors.New("reading error body: " + err.Error())
}
- } else if response.Error == "" {
+ call.done()
+ default:
err = client.codec.ReadResponseBody(call.Reply)
if err != nil {
call.Error = errors.New("reading body " + err.Error())
}
- }
- if call != nil {
call.done()
}
}
コアとなるコードの解説
変更の中心は、client.go
ファイル内の Client.input()
メソッドです。このメソッドは、RPCクライアントがサーバーからの応答を非同期的に処理するゴルーチン内で実行されます。
以前のコードでは、応答の処理は主に if call == nil || response.Error != ""
という条件分岐と、それに続く else if response.Error == ""
という形で実装されていました。この構造は、特に call != nil
のチェックが複数箇所に散らばっていたため、コードの意図を把握しにくくしていました。
新しいコードでは、この複雑な条件分岐がGoの「タグなし switch
ステートメント」に置き換えられています。タグなし switch
は、条件式を持たず、各 case
の式がブール値として評価され、最初に true
になった case
ブロックが実行されます。これにより、応答処理のロジックが以下の3つの明確なパスに分割されました。
-
case call == nil:
- このケースは、サーバーからの応答に対応する保留中のRPC呼び出し (
call
) が見つからなかった状況を扱います。これは、クライアントがリクエストを送信する際に何らかの理由で部分的に失敗し、call
オブジェクトが既にclient.pending
マップから削除されている場合に発生する可能性があります。サーバーは、リクエストボディの読み取りエラーなどについて応答を返してくることがありますが、クライアント側にはその応答を関連付けるcall
がもう存在しません。 - この場合、
client.codec.ReadResponseBody(nil)
を呼び出して、応答ボディを読み飛ばし、破棄します。これは、後続のRPC通信が正しく行われるように、ネットワークストリームから不要なデータをクリアするためです。もしボディの読み取り中にエラーが発生した場合、そのエラーは記録されますが、特定のcall
に割り当てることはできません。
- このケースは、サーバーからの応答に対応する保留中のRPC呼び出し (
-
case response.Error != "":
- このケースは、サーバーからエラー応答が返された状況を扱います。
response.Error
フィールドにエラーメッセージが含まれている場合です。この場合、対応するcall
オブジェクトは存在します。 call.Error = ServerError(response.Error)
を設定することで、サーバーからのエラーメッセージをクライアント側のcall
オブジェクトに伝播させます。ServerError
は、net/rpc
パッケージで定義されているエラー型で、サーバー側で発生したエラーであることを示します。- ここでも
client.codec.ReadResponseBody(nil)
を呼び出して応答ボディを破棄します。エラー応答の場合、通常は有効な応答ボディは期待されないためです。 - 最後に
call.done()
を呼び出します。これは、このRPC呼び出しが完了したことを通知し、Call.Done
チャネルをクローズすることで、Go()
メソッドなどで待機しているゴルーチンを解放します。
- このケースは、サーバーからエラー応答が返された状況を扱います。
-
default:
- このケースは、上記の2つのケース(
call == nil
またはresponse.Error != ""
)のいずれにも当てはまらない状況を扱います。これは、サーバーから正常な応答が返され、対応するcall
オブジェクトも存在する場合を意味します。 err = client.codec.ReadResponseBody(call.Reply)
を呼び出して、サーバーからの応答ボディをcall.Reply
フィールドにデコードします。call.Reply
は、RPC呼び出しの引数として渡された、結果を格納するためのポインタです。- 応答ボディの読み取り中にエラーが発生した場合(例: ネットワークエラー、デコードエラー)、そのエラーは
call.Error
に設定されます。 - 最後に
call.done()
を呼び出して、このRPC呼び出しが正常に完了したことを通知します。
- このケースは、上記の2つのケース(
この switch
ステートメントへの変更により、応答処理の各シナリオが明確に分離され、コードのフローがより理解しやすくなりました。特に、以前のコードで散見された if call != nil
のような冗長なチェックが不要になり、コードの簡潔性と堅牢性が向上しています。
関連リンク
- https://github.com/golang/go/commit/dcc80e4553e4a9a9676d0fd35092cc1009bc148c
- https://golang.org/cl/6248048
参考にした情報源リンク
- Go言語の
net/rpc
パッケージに関する公式ドキュメントやチュートリアル (一般的なRPCの概念とGoでの実装について) - Go言語の
switch
ステートメントに関する公式ドキュメント (タグなしswitch
の動作について) CL 5956051
に関する情報 (このコミットの背景にある以前の変更について)- https://go-review.googlesource.com/c/go/+/5956051 (Go Gerrit Code Review)