[インデックス 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 Register
(net/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サービスとして登録される型は、以下の条件を満たすメソッドを公開する必要があります。
- メソッド名がエクスポートされている(大文字で始まる)。
- メソッドが3つの引数を持つ:
(receiver, args, reply)
。 args
はエクスポートされた型または組み込み型である。reply
はポインタ型であり、エクスポートされた型または組み込み型である。- メソッドが1つの戻り値を持つ。
- 戻り値の型が
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{}))
は*MyStruct
のreflect.Type
を返します。
Goのレシーバとメソッドセット
Goでは、メソッドは値レシーバ (func (t T) Method()
) またはポインタレシーバ (func (t *T) Method()
) を持つことができます。
- 値型
T
のメソッドセットには、値レシーバを持つメソッドのみが含まれます。 - ポインタ型
*T
のメソッドセットには、値レシーバとポインタレシーバの両方を持つメソッドが含まれます。
この違いが、rpc.Register
に値型を渡した際に、ポインタレシーバのメソッドが認識されない原因となっていました。
技術的詳細
このコミットの主要な変更点は、net/rpc/server.go
内の Server.register
メソッドのロジックにあります。
-
suitableMethods
関数の導入:- 以前は
Server.register
メソッド内に直接記述されていた、RPCに適格なメソッドを探索するロジックが、新しくsuitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType
というヘルパー関数として抽出されました。 - この関数は、与えられた
reflect.Type
から、RPCの要件を満たすメソッドを抽出し、map[string]*methodType
として返します。 reportErr
引数は、メソッドがRPCの要件を満たさない場合にlog.Println
でエラーメッセージを出力するかどうかを制御します。これにより、ヒントを生成するためにポインタ型を試す際に、不要なログ出力が抑制されます。
- 以前は
-
ポインタヒントの追加ロジック:
Server.register
メソッド内で、まず引数として渡されたレシーバの型 (s.typ
) に対してsuitableMethods
を呼び出し、RPCに適格なメソッドを探します。- もし
s.typ
からRPCに適格なメソッドが一つも見つからなかった場合 (len(s.method) == 0
)、以下の追加チェックが行われます。reflect.PtrTo(s.typ)
を使用して、元の型のポインタ型を取得します。- このポインタ型に対して
suitableMethods
を呼び出します。この際、reportErr
はfalse
に設定され、ログの出力を抑制します。 - もしポインタ型からRPCに適格なメソッドが見つかった場合 (
len(method) != 0
)、エラーメッセージに(hint: pass a pointer to value of that type)
というヒントが追加されます。 - これにより、開発者は「値型を登録したが、ポインタ型であればメソッドが見つかる」という状況を明確に理解できるようになります。
-
エラーメッセージの改善:
- ヒントが追加されることで、以前の「適切な型の公開されたメソッドがありません」という一般的なエラーメッセージが、より具体的で役立つ情報を含むようになります。
-
テストケースの追加:
src/pkg/net/rpc/server_test.go
にNeedsPtrType
という新しい型が追加されました。この型は、ポインタレシーバを持つ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
パッケージのユーザビリティを大幅に向上させ、開発者が遭遇しがちな一般的な落とし穴を回避するのに役立ちます。
関連リンク
- Go Issue #4325: https://github.com/golang/go/issues/4325
- Go Code Review: https://golang.org/cl/6819081
参考にした情報源リンク
- Go
net/rpc
パッケージドキュメント: https://pkg.go.dev/net/rpc - Go
reflect
パッケージドキュメント: https://pkg.go.dev/reflect - A Tour of Go - Methods: https://go.dev/tour/methods/1
- Go by Example: Methods: https://gobyexample.com/methods
- The Go Programming Language Specification - Method sets: https://go.dev/ref/spec#Method_sets
- Go Issue Tracker: https://github.com/golang/go/issues
- Go Code Review Dashboard: https://go.dev/cl/