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

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

このコミットは、Go言語の標準ライブラリである net/rpc パッケージにおける Register 関数の挙動を改善するものです。具体的には、RPCサービスとして登録しようとする型が、ポインタレシーバを持つメソッドしかRPCに適格なメソッドとして公開していない場合に、より分かりやすいエラーメッセージ(ポインタを渡すよう促すヒント)を提供するようになります。これにより、開発者が net/rpc を利用する際のデバッグ体験が向上します。

コミット

commit 0e3f4fdb520097b9c45264ebc97e246a156b51d2
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Wed Nov 7 05:03:16 2012 +0800

    net/rpc: give hint to pass a pointer to Register
    Fixes #4325.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6819081

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

https://github.com/golang/go/commit/0e3f4fdb520097b9c45264ebc97e246a156b51d2

元コミット内容

net/rpc: give hint to pass a pointer to Registernet/rpc: Register にポインタを渡すようヒントを与える)

このコミットは、GoのRPCシステムにおいて、サービス登録時にポインタレシーバを持つメソッドが期待される場合に、その旨をエラーメッセージで示唆するように変更します。これにより、rpc.Register に値型を渡してしまい、RPCメソッドが認識されないという一般的な間違いに対するユーザビリティが向上します。

変更の背景

Goの net/rpc パッケージでは、RPCサービスとして登録される型は、特定のシグネチャを持つメソッドを公開する必要があります。これらのメソッドは通常、レシーバとしてポインタ型を取ることが一般的です。しかし、Goのメソッドは値レシーバとポインタレシーバの両方を持つことができ、それぞれ異なるメソッドセットを定義します。

rpc.Register 関数に値型(例: MyService{})を渡した場合、その値型に定義されたメソッドのみがRPCサービスとして考慮されます。もし、RPCに適格なメソッドがポインタレシーバ(例: func (s *MyService) MyMethod(...))としてのみ定義されている場合、rpc.Register はその値型からはRPCメソッドを見つけることができず、「適切な型の公開されたメソッドがありません」というエラーを返していました。

このエラーメッセージは正確ではあるものの、なぜメソッドが見つからないのか、特にポインタレシーバのメソッドが存在する場合に、開発者にとっては不親切でした。多くの開発者がこの挙動で混乱し、デバッグに時間を要していました。

この問題は、GoのIssueトラッカーで #4325 として報告されていました。このコミットは、そのIssueを解決するために、RPCに適格なメソッドが見つからなかった場合に、その型のポインタ型を試してみて、もしポインタ型であればRPCメソッドが見つかる場合に、その旨をエラーメッセージに含めることで、開発者へのヒントを提供するものです。

前提知識の解説

Goの net/rpc パッケージ

net/rpc は、Go言語でRPC(Remote Procedure Call)サービスを簡単に構築するための標準パッケージです。クライアントがネットワーク経由でリモートの関数を呼び出すことを可能にします。 RPCサービスとして登録される型は、以下の条件を満たすメソッドを公開する必要があります。

  1. メソッド名がエクスポートされている(大文字で始まる)。
  2. メソッドが3つの引数を持つ: (receiver, args, reply)
  3. args はエクスポートされた型または組み込み型である。
  4. reply はポインタ型であり、エクスポートされた型または組み込み型である。
  5. メソッドが1つの戻り値を持つ。
  6. 戻り値の型が error である。

Goの reflect パッケージ

reflect パッケージは、実行時にプログラムの構造を検査し、操作するための機能を提供します。このコミットでは特に以下の機能が利用されています。

  • reflect.Type: Goの型の情報を表すインターフェース。
  • Type.NumMethod(): 型が持つメソッドの数を返します。
  • Type.Method(i): 指定されたインデックスのメソッドを reflect.Method 型で返します。
  • Method.Type: メソッドの関数型を reflect.Type で返します。
  • Type.NumIn(): 関数の入力引数の数を返します。
  • Type.In(i): 関数の指定されたインデックスの入力引数の型を reflect.Type で返します。
  • Type.NumOut(): 関数の出力引数の数を返します。
  • Type.Out(i): 関数の指定されたインデックスの出力引数の型を reflect.Type で返します。
  • reflect.PtrTo(t reflect.Type): 指定された型 t へのポインタ型を返します。例えば、reflect.PtrTo(reflect.TypeOf(MyStruct{}))*MyStructreflect.Type を返します。

Goのレシーバとメソッドセット

Goでは、メソッドは値レシーバ (func (t T) Method()) またはポインタレシーバ (func (t *T) Method()) を持つことができます。

  • 値型 T のメソッドセットには、値レシーバを持つメソッドのみが含まれます。
  • ポインタ型 *T のメソッドセットには、値レシーバとポインタレシーバの両方を持つメソッドが含まれます。

この違いが、rpc.Register に値型を渡した際に、ポインタレシーバのメソッドが認識されない原因となっていました。

技術的詳細

このコミットの主要な変更点は、net/rpc/server.go 内の Server.register メソッドのロジックにあります。

  1. suitableMethods 関数の導入:

    • 以前は Server.register メソッド内に直接記述されていた、RPCに適格なメソッドを探索するロジックが、新しく suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType というヘルパー関数として抽出されました。
    • この関数は、与えられた reflect.Type から、RPCの要件を満たすメソッドを抽出し、map[string]*methodType として返します。
    • reportErr 引数は、メソッドがRPCの要件を満たさない場合に log.Println でエラーメッセージを出力するかどうかを制御します。これにより、ヒントを生成するためにポインタ型を試す際に、不要なログ出力が抑制されます。
  2. ポインタヒントの追加ロジック:

    • Server.register メソッド内で、まず引数として渡されたレシーバの型 (s.typ) に対して suitableMethods を呼び出し、RPCに適格なメソッドを探します。
    • もし s.typ からRPCに適格なメソッドが一つも見つからなかった場合 (len(s.method) == 0)、以下の追加チェックが行われます。
      • reflect.PtrTo(s.typ) を使用して、元の型のポインタ型を取得します。
      • このポインタ型に対して suitableMethods を呼び出します。この際、reportErrfalse に設定され、ログの出力を抑制します。
      • もしポインタ型からRPCに適格なメソッドが見つかった場合 (len(method) != 0)、エラーメッセージに (hint: pass a pointer to value of that type) というヒントが追加されます。
      • これにより、開発者は「値型を登録したが、ポインタ型であればメソッドが見つかる」という状況を明確に理解できるようになります。
  3. エラーメッセージの改善:

    • ヒントが追加されることで、以前の「適切な型の公開されたメソッドがありません」という一般的なエラーメッセージが、より具体的で役立つ情報を含むようになります。
  4. テストケースの追加:

    • src/pkg/net/rpc/server_test.goNeedsPtrType という新しい型が追加されました。この型は、ポインタレシーバを持つRPCに適格なメソッド NeedsPtrType を定義しています。
    • TestRegistrationError テスト関数に、Register(NeedsPtrType(0)) (値型)を呼び出す新しいテストケースが追加されました。
    • このテストケースは、Register がエラーを返し、そのエラーメッセージに「pointer」という文字列が含まれていることを検証します。これにより、ヒント機能が正しく動作していることが保証されます。

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

src/pkg/net/rpc/server.go

 // 変更前 (抜粋)
 func (server *Server) register(rcvr interface{}, name string, useName bool) error {
 	s := new(service)
 	s.typ = reflect.TypeOf(rcvr)
 	s.rcvr = reflect.ValueOf(rcvr)
 	sname := name
 	if useName {
 		sname = reflect.Indirect(s.rcvr).Type().Name()
 	}
 	if sname == "" {
 		return errors.New("rpc.Register: no service name for type " + s.typ.String())
 	}
 	if !isExported(sname) {
 		return errors.New("rpc.Register: type " + sname + " is not exported")
 	}
 	s.name = sname
 	s.method = make(map[string]*methodType)

 	// Install the methods
 	for m := 0; m < s.typ.NumMethod(); m++ {
 		method := s.typ.Method(m)
 		mtype := method.Type
 		mname := method.Name
 		// ... (メソッドの条件チェックロジック) ...
 		s.method[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType}
 	}

 	if len(s.method) == 0 {
 		s := "rpc Register: type " + sname + " has no exported methods of suitable type"
 		log.Print(s)
 		return errors.New(s)
 	}
 	server.serviceMap[s.name] = s
 	return nil
 }
 // 変更後 (抜粋)
 func (server *Server) register(rcvr interface{}, name string, useName bool) error {
 	s := new(service)
 	s.typ = reflect.TypeOf(rcvr)
 	s.rcvr = reflect.ValueOf(rcvr)
 	sname := name
 	if useName {
 		sname = reflect.Indirect(s.rcvr).Type().Name()
 	}
 	if sname == "" {
 		return errors.New("rpc.Register: no service name for type " + s.typ.String())
 	}
 	if !isExported(sname) {
 		return errors.New("rpc.Register: type " + sname + " is not exported")
 	}
 	s.name = sname
 	s.method = make(map[string]*methodType)

 	// Install the methods
 	s.method = suitableMethods(s.typ, true) // <-- suitableMethods を呼び出す

 	if len(s.method) == 0 {
 		str := ""
 		// To help the user, see if a pointer receiver would work.
 		method := suitableMethods(reflect.PtrTo(s.typ), false) // <-- ポインタ型を試す
 		if len(method) != 0 {
 			str = "rpc.Register: type " + sname + " has no exported methods of suitable type (hint: pass a pointer to value of that type)" // <-- ヒントを追加
 		} else {
 			str = "rpc.Register: type " + sname + " has no exported methods of suitable type"
 		}
 		log.Print(str)
 		return errors.New(str)
 	}
 	server.serviceMap[s.name] = s
 	return nil
 }

 // 新しく追加された関数
 // suitableMethods returns suitable Rpc methods of typ, it will report
 // error using log if reportErr is true.
 func suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType {
 	methods := make(map[string]*methodType)
 	for m := 0; m < typ.NumMethod(); m++ {
 		method := typ.Method(m)
 		mtype := method.Type
 		mname := method.Name
 		// Method must be exported.
 		if method.PkgPath != "" {
 			continue
 		}
 		// Method needs three ins: receiver, *args, *reply.
 		if mtype.NumIn() != 3 {
 			if reportErr { // <-- reportErr に応じてログ出力
 				log.Println("method", mname, "has wrong number of ins:", mtype.NumIn())
 			}
 			continue
 		}
 		// ... (他の条件チェックも同様に reportErr に応じてログ出力) ...
 		methods[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType}
 	}
 	return methods
 }

src/pkg/net/rpc/server_test.go

 // 変更前 (抜粋)
 func TestRegistrationError(t *testing.T) {
 	err := Register(new(ReplyNotPointer))
 	if err == nil {
 		t.Errorf("expected error registering ReplyNotPointer")
 	}
 	err = Register(new(ArgNotPublic))
 	if err == nil {
 		t.Errorf("expected error registering ArgNotPublic")
 	}
 	err = Register(new(ReplyNotPublic))
 	if err == nil {
 		t.Errorf("expected error registering ReplyNotPublic")
 	}
 }
 // 変更後 (抜粋)
 type NeedsPtrType int // <-- 新しいテスト用型

 func (t *NeedsPtrType) NeedsPtrType(args *Args, reply *Reply) error { // <-- ポインタレシーバを持つメソッド
 	return nil
 }

 func TestRegistrationError(t *testing.T) {
 	err := Register(new(ReplyNotPointer))
 	if err == nil {
 		t.Error("expected error registering ReplyNotPointer")
 	}
 	err = Register(new(ArgNotPublic))
 	if err == nil {
 		t.Error("expected error registering ArgNotPublic")
 	}
 	err = Register(new(ReplyNotPublic))
 	if err == nil {
 		t.Error("expected error registering ReplyNotPublic")
 	}
 	err = Register(NeedsPtrType(0)) // <-- 値型を登録
 	if err == nil {
 		t.Error("expected error registering NeedsPtrType")
 	} else if !strings.Contains(err.Error(), "pointer") { // <-- エラーメッセージに "pointer" が含まれることを確認
 		t.Error("expected hint when registering NeedsPtrType")
 	}
 }

コアとなるコードの解説

server.go の変更

  • suitableMethods の抽出: Server.register 関数から、RPCメソッドの適合性をチェックするロジックが suitableMethods という独立した関数に切り出されました。これにより、コードの再利用性が高まり、register 関数の可読性が向上しました。
  • reportErr パラメータ: suitableMethods 関数に reportErr というブール値のパラメータが追加されました。これは、メソッドがRPCの条件を満たさない場合に log.Println でエラーメッセージを出力するかどうかを制御します。このパラメータは、ヒントを生成するためにポインタ型を試す際に、不要なログ出力を避けるために false で呼び出されます。
  • ポインタヒントのロジック: Server.register 内で、まず元の型 (s.typ) でRPCメソッドを検索します。もし見つからなかった場合、reflect.PtrTo(s.typ) を使ってその型のポインタ型を取得し、再度 suitableMethods で検索します。
    • もしポインタ型でメソッドが見つかった場合、rpc.Register: type [TypeName] has no exported methods of suitable type (hint: pass a pointer to value of that type) という形式の、より具体的なエラーメッセージが生成されます。
    • ポインタ型でもメソッドが見つからなかった場合は、従来の一般的なエラーメッセージが使用されます。
  • この変更により、開発者が net/rpc を使用する際に、ポインタレシーバのメソッドを持つサービスを値型で登録しようとした場合に、すぐに問題の原因と解決策を理解できるようになります。

server_test.go の変更

  • NeedsPtrType の追加: NeedsPtrType という新しい整数型が定義され、そのポインタレシーバ (*NeedsPtrType) に NeedsPtrType というRPCに適格なメソッドが追加されました。これは、値型ではRPCメソッドを持たないが、ポインタ型では持つというシナリオをシミュレートするためのものです。
  • TestRegistrationError の拡張:
    • Register(NeedsPtrType(0)) という呼び出しが追加されました。ここで NeedsPtrType(0) は値型であり、RPCに適格なメソッドはポインタレシーバにのみ存在するため、登録は失敗するはずです。
    • テストは、Register がエラーを返すことを確認するだけでなく、返されたエラーメッセージが strings.Contains(err.Error(), "pointer") を満たす、つまり「pointer」というキーワードを含んでいることを検証します。これにより、新しいヒント機能が期待通りに動作していることが保証されます。

これらの変更は、Goの net/rpc パッケージのユーザビリティを大幅に向上させ、開発者が遭遇しがちな一般的な落とし穴を回避するのに役立ちます。

関連リンク

参考にした情報源リンク