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

[インデックス 16715] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/rpc パッケージ内の server.go ファイルに対する変更です。具体的には、サービス登録時のエラー処理において、log.Fatal の使用を log.Print とエラーの返却に置き換えることで、より堅牢でGoらしいエラーハンドリングを実現しています。

コミット

commit e23d19e23535837fad3d1095e48fe877dbb38c10
Author: ChaiShushan <chaishushan@gmail.com>
Date:   Tue Jul 9 11:12:05 2013 +1000

    net/rpc: use log.Print and return error instead log.Fatal
    
    R=r
    CC=golang-dev
    https://golang.org/cl/10758044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e23d19e23535837fad3d1095e48fe877dbb38c10

元コミット内容

net/rpc: use log.Print and return error instead log.Fatal

変更の背景

この変更の背景には、Go言語におけるエラーハンドリングの哲学と、ライブラリ設計におけるベストプラクティスがあります。

Go言語では、エラーは通常、関数の戻り値として明示的に扱われます。これにより、呼び出し元がエラーの発生を検知し、適切に処理する機会が与えられます。しかし、元のコードでは log.Fatal が使用されていました。log.Fatal はメッセージをログに出力した後、os.Exit(1) を呼び出してプログラム全体を即座に終了させる関数です。

net/rpc のようなライブラリの内部で log.Fatal を使用すると、以下のような問題が発生します。

  1. 予期せぬプログラム終了: ライブラリの内部で発生したエラーが、アプリケーション全体を強制終了させてしまいます。これにより、アプリケーションはエラーから回復する機会を失い、予期せぬダウンタイムやデータ損失につながる可能性があります。
  2. エラーハンドリングの妨げ: 呼び出し元は log.Fatal によってプログラムが終了するため、エラーを捕捉したり、代替処理を実行したり、クリーンアップ処理(defer ステートメントなど)を実行したりすることができません。
  3. テストの困難さ: log.Fatal はテスト中にプログラムを終了させるため、エラーパスのテストが困難になります。

このコミットは、これらの問題を解決し、net/rpc パッケージがGoのエラーハンドリングの慣習に沿うようにするために行われました。エラーをログに出力する代わりに log.Print を使用し、さらに error 型の値を返すことで、呼び出し元がエラーを適切に処理できるようにしています。

前提知識の解説

Goの net/rpc パッケージ

net/rpc パッケージは、Go言語でリモートプロシージャコール (RPC) を実装するためのメカニズムを提供します。これにより、クライアントはネットワーク経由でリモートサーバー上のオブジェクトのメソッドを呼び出すことができます。

  • クライアント・サーバーモデル: net/rpc はクライアント・サーバーアーキテクチャをサポートします。サーバーはサービス(エクスポートされたメソッドを持つオブジェクト)を登録し、クライアントはこれらのメソッドを呼び出すために接続を確立します。
  • サービス登録: サーバーは rpc.Register() を呼び出すことで、オブジェクトのメソッドをリモートアクセス可能にします。オブジェクトは、その型名でサービスとして公開されます。
  • メソッドの要件: リモートからアクセス可能なメソッドは、以下の条件を満たす必要があります。
    • エクスポートされたメソッドであること(大文字で始まる)。
    • エクスポートされた型(例: 大文字で始まる構造体)のメソッドであること。
    • 2つの引数を持ち、両方ともエクスポートされた型であること。
    • 2番目の引数はポインタであること。
    • 単一の error 型を返すこと。
    • 一般的なメソッドのシグネチャは func (t *T) MethodName(argType T1, replyType *T2) error です。
  • データエンコーディング: デフォルトでは、net/rpc パッケージは encoding/gob を使用してデータを転送します。gob はGo固有のエンコーディングであるため、net/rpc は主にGo-to-Go間の通信に適しています。異なる言語間でのRPCには、JSON-RPC (net/rpc/jsonrpc) や gRPC のような代替手段が一般的に使用されます。
  • 現状: net/rpc パッケージは「凍結」されており、新しい機能は追加されていません。現代の高性能で言語横断的なRPCには、gRPCが広く利用されています。

Goの log パッケージにおける log.Fatallog.Print の違い

Goの標準 log パッケージには、メッセージを出力するためのいくつかの関数がありますが、log.Fatallog.Print はその挙動が大きく異なります。

  • log.Print (および log.Println, log.Printf):

    • 挙動: メッセージを標準ロガーの出力(デフォルトでは os.Stderr)に表示した後、プログラムの実行を継続します。
    • 目的: 情報メッセージ、デバッグ出力、またはプログラムの終了を必要としない非致命的なエラーに使用されます。
    • defer ステートメント: log.Print の後もプログラムは継続するため、defer ステートメントは通常通り実行されます。
  • log.Fatal (および log.Fatalln, log.Fatalf):

    • 挙動: メッセージを標準ロガーの出力に表示した後、直ちに os.Exit(1) を呼び出してプログラムを終了させます。終了ステータスは1で、エラーを示します。
    • 目的: プログラムが回復できないような致命的なエラー、例えば設定ファイルの読み込み失敗やデータベース接続の確立失敗など、即座にプログラムを停止する必要がある場合に使用されます。
    • defer ステートメント: os.Exit(1) が呼び出されると、プログラム内のいかなる defer ステートメントも実行されません。これは、ファイルやデータベース接続のクリーンアップ操作がスキップされる可能性があることを意味します。

Goにおけるエラーハンドリングのベストプラクティス

Goのエラーハンドリングは、例外を使用する他の多くの言語とは異なり、エラーを値として扱うことで明示的なエラーチェックと処理を促進します。

  • エラーを戻り値として返す: エラーが発生する可能性のある関数は、最後の戻り値として error 型を返すべきです。エラーがない場合は nil を返します。これがGoにおける最も一般的で慣用的なエラー処理方法です。
  • エラーの即時チェック: エラーを返す関数呼び出しの後には、常に if err != nil パターンでエラーを即座にチェックします。
  • panic の限定的な使用: panicrecover は、Goにおける真に例外的な、回復不能なエラーを処理するためのメカニズムです。しかし、これらはごく稀にしか使用すべきではありません。プログラムが継続できないような壊滅的な状況に限定して使用し、予期されるエラーには error 値を返すようにします。
  • エラーの伝播: エラーを呼び出しスタックの上位に伝播させる場合、fmt.Errorf%w 動詞(Go 1.13以降)を使用して、元のエラーに追加のコンテキストをラップします。これにより、元のエラーを検査しつつ、エラーが発生した場所の明確なトレイルを提供できます。
  • ログとリターンの分離: エラーは理想的には一度だけ処理されるべきです。関数がエラーに遭遇した場合、それを処理するか(例えば、最上位で致命的なエラーであればログに記録して実行を停止する)、または呼び出し元に返すかのいずれかを行うべきです。エラーをログに記録してから返すことは、重複したログ出力につながり、デバッグを困難にする可能性があります。

技術的詳細

このコミットは、net/rpc パッケージの (*Server).register メソッドにおけるエラー処理の改善に焦点を当てています。このメソッドは、RPCサービスをサーバーに登録する際に使用されます。

元の実装では、サービス名が空である場合や、サービス名がエクスポートされていない(Goの命名規則に従っていない)場合に、log.Fatal を呼び出してプログラムを強制終了させていました。これは、前述の通りライブラリの挙動としては非常に問題があります。ライブラリは、自身のエラーによってアプリケーション全体を終了させるべきではありません。代わりに、エラーを呼び出し元に返し、アプリケーションがそのエラーを適切に処理できるようにするべきです。

この変更により、以下の点が改善されます。

  1. プログラムの安定性: log.Fatal の削除により、net/rpc サービス登録時の不正な入力が原因でアプリケーション全体がクラッシュするのを防ぎます。
  2. エラーの伝播と処理: errors.New(s) を使用してエラーオブジェクトを生成し、それを関数の戻り値として返すことで、register メソッドの呼び出し元(通常はアプリケーションコード)がエラーを捕捉し、ログ記録、ユーザーへの通知、代替処理の実行など、適切なエラーハンドリングロジックを適用できるようになります。
  3. デバッグとテストの容易性: エラーが戻り値として返されるため、単体テストで特定のエラーシナリオをシミュレートし、そのエラーが適切に処理されることを検証することが容易になります。また、アプリケーションのデバッグ時にも、エラーの発生源と伝播経路を追跡しやすくなります。
  4. Goの慣習への準拠: この変更は、Go言語における「エラーは値である」という哲学と、ライブラリは paniclog.Fatal のようなプログラムを終了させるメカニズムを避けるべきであるという慣習に完全に準拠しています。

具体的には、サービス名が空の場合と、サービス名がエクスポートされていない場合の2つのエラーケースで変更が行われています。どちらのケースでも、エラーメッセージを構築し、それを log.Print で出力し、最後に errors.New で新しいエラーを生成して返しています。これにより、エラーの発生をログに記録しつつ、プログラムの実行を継続し、呼び出し元にエラー処理を委ねるという、より望ましい挙動を実現しています。

コアとなるコードの変更箇所

--- a/src/pkg/net/rpc/server.go
+++ b/src/pkg/net/rpc/server.go
@@ -247,10 +247,12 @@ func (server *Server) register(rcvr interface{}, name string, useName bool) erro
 		sname = name
 	}
 	if sname == "" {
-		log.Fatal("rpc: no service name for type", s.typ.String())
+		s := "rpc.Register: no service name for type " + s.typ.String()
+		log.Print(s)
+		return errors.New(s)
 	}
 	if !isExported(sname) && !useName {
-		s := "rpc Register: type " + sname + " is not exported"
+		s := "rpc.Register: type " + sname + " is not exported"
 		log.Print(s)
 		return errors.New(s)
 	}

コアとなるコードの解説

変更は src/pkg/net/rpc/server.go ファイルの func (server *Server) register(...) error メソッド内で行われています。

  1. サービス名が空の場合の変更:

    -		log.Fatal("rpc: no service name for type", s.typ.String())
    +		s := "rpc.Register: no service name for type " + s.typ.String()
    +		log.Print(s)
    +		return errors.New(s)
    
    • 変更前: log.Fatal を使用して、サービス名が空であることを示すメッセージをログに出力し、プログラムを強制終了していました。
    • 変更後:
      • まず、エラーメッセージ文字列 s を構築します。このメッセージは、元の log.Fatal が出力していた内容とほぼ同じです。
      • log.Print(s) を呼び出して、エラーメッセージをログに出力します。log.Print はプログラムを終了させません。
      • return errors.New(s) を使用して、構築したエラーメッセージを含む新しい error オブジェクトを生成し、それを関数の戻り値として返します。これにより、呼び出し元がこのエラーを捕捉し、適切に処理できるようになります。
  2. サービス名がエクスポートされていない場合の変更:

    -		s := "rpc Register: type " + sname + " is not exported"
    +		s := "rpc.Register: type " + sname + " is not exported"
    		log.Print(s)
    		return errors.New(s)
    
    • 変更前: log.Print でエラーメッセージを出力し、errors.New(s) でエラーを返していました。しかし、エラーメッセージの文字列リテラルが rpc Register となっており、一貫性がありませんでした。
    • 変更後:
      • エラーメッセージ文字列 s の内容を rpc.Register に修正し、より一貫性のある形式にしています。
      • log.Print(s)return errors.New(s) の組み合わせは変更されていませんが、これは既にGoのエラーハンドリングの慣習に沿った正しいアプローチです。この変更は、主にメッセージの一貫性を保つためのものです。

これらの変更により、net/rpc パッケージは、サービス登録時のエラーをよりGoらしい方法で扱い、アプリケーションの堅牢性とエラー処理の柔軟性を向上させています。

関連リンク

参考にした情報源リンク