Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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の基本的な流れは以下の通りです。

  1. クライアントが呼び出し: クライアントはリモートの関数を呼び出します。
  2. 引数のマーシャリング: クライアント側で引数がネットワーク経由で送信可能な形式(例: JSON、Gob)にシリアライズ(マーシャリング)されます。
  3. ネットワーク送信: シリアライズされた引数がネットワーク経由でサーバーに送信されます。
  4. 引数のアンマーシャリング: サーバー側で引数がデシリアライズ(アンマーシャリング)されます。
  5. サーバーでの実行: サーバーは指定された関数を実行します。
  6. 結果のマーシャリング: サーバー側で結果がシリアライズされます。
  7. ネットワーク送信: シリアライズされた結果がネットワーク経由でクライアントに送信されます。
  8. 結果のアンマーシャリング: クライアント側で結果がデシリアライズされます。
  9. クライアントでの受け取り: クライアントは結果を受け取ります。

client.goinput() メソッド

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.goinput() メソッド内における応答処理ロジックの再構築です。以前は複数の if-else ifcall != nil チェックが散在していましたが、これを単一の switch ステートメントに集約し、3つの明確なケースに分割しています。

変更前のコードは、主に以下の2つの大きな if ブロックで構成されていました。

  1. call == nil || response.Error != "" の場合(エラー応答または対応する呼び出しがない場合)
  2. response.Error == "" の場合(正常応答の場合)

これらのブロック内でさらに call != nil のチェックが行われており、ロジックが複雑になっていました。

変更後のコードでは、以下の3つの case を持つ switch ステートメントが導入されています。

  1. case call == nil:

    • 意味: サーバーからの応答に対応する保留中のRPC呼び出し (call) が見つからなかった場合。これは通常、クライアントがリクエストを送信する際に部分的に失敗し、call が既に削除されている状況で、サーバーがリクエストボディの読み取りエラーについて応答を返してきた場合に発生します。
    • 処理: この場合、応答ボディは破棄されます (client.codec.ReadResponseBody(nil))。エラーが発生した場合は、そのエラーが記録されますが、call が存在しないため、特定の呼び出しにエラーを割り当てることはできません。
    • 改善点: 以前は if call == nil || response.Error != "" の一部として処理され、その後の if call != nil との組み合わせで混乱を招く可能性がありました。このケースを独立させることで、この特定のシナリオの意図が明確になります。
  2. case response.Error != "":

    • 意味: サーバーからエラー応答が返された場合。call は存在しますが、サーバー側で処理中にエラーが発生したことを示します。
    • 処理: call.ErrorServerError(response.Error) を設定し、サーバーからのエラーメッセージをRPC呼び出しに伝播します。その後、応答ボディは破棄されます (client.codec.ReadResponseBody(nil))。応答ボディの読み取り中にエラーが発生した場合も、そのエラーが記録されます。最後に call.done() を呼び出して、このRPC呼び出しが完了したことを通知します。
    • 改善点: 以前は if call == nil || response.Error != "" の一部として処理され、if call != nil の内部でエラーが設定されていました。このケースを独立させることで、エラー応答の処理パスが明確になり、call != nil の冗長なチェックが不要になります。
  3. 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つの明確なパスに分割されました。

  1. case call == nil:

    • このケースは、サーバーからの応答に対応する保留中のRPC呼び出し (call) が見つからなかった状況を扱います。これは、クライアントがリクエストを送信する際に何らかの理由で部分的に失敗し、call オブジェクトが既に client.pending マップから削除されている場合に発生する可能性があります。サーバーは、リクエストボディの読み取りエラーなどについて応答を返してくることがありますが、クライアント側にはその応答を関連付ける call がもう存在しません。
    • この場合、client.codec.ReadResponseBody(nil) を呼び出して、応答ボディを読み飛ばし、破棄します。これは、後続のRPC通信が正しく行われるように、ネットワークストリームから不要なデータをクリアするためです。もしボディの読み取り中にエラーが発生した場合、そのエラーは記録されますが、特定の call に割り当てることはできません。
  2. 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() メソッドなどで待機しているゴルーチンを解放します。
  3. default:

    • このケースは、上記の2つのケース(call == nil または response.Error != "")のいずれにも当てはまらない状況を扱います。これは、サーバーから正常な応答が返され、対応する call オブジェクトも存在する場合を意味します。
    • err = client.codec.ReadResponseBody(call.Reply) を呼び出して、サーバーからの応答ボディを call.Reply フィールドにデコードします。call.Reply は、RPC呼び出しの引数として渡された、結果を格納するためのポインタです。
    • 応答ボディの読み取り中にエラーが発生した場合(例: ネットワークエラー、デコードエラー)、そのエラーは call.Error に設定されます。
    • 最後に call.done() を呼び出して、このRPC呼び出しが正常に完了したことを通知します。

この switch ステートメントへの変更により、応答処理の各シナリオが明確に分離され、コードのフローがより理解しやすくなりました。特に、以前のコードで散見された if call != nil のような冗長なチェックが不要になり、コードの簡潔性と堅牢性が向上しています。

関連リンク

参考にした情報源リンク

  • Go言語の net/rpc パッケージに関する公式ドキュメントやチュートリアル (一般的なRPCの概念とGoでの実装について)
  • Go言語の switch ステートメントに関する公式ドキュメント (タグなし switch の動作について)
  • CL 5956051 に関する情報 (このコミットの背景にある以前の変更について)
  • https://go-review.googlesource.com/c/go/+/5956051 (Go Gerrit Code Review)