原文地址:https://tonybai.com/2021/09/26/the-design-of-the-response-for-grpc-server/
1. 服務(wù)端響應(yīng)的現(xiàn)狀
做后端服務(wù)的開發(fā)人員對(duì)錯(cuò)誤處理總是很敏感的,因此在做服務(wù)的響應(yīng)(response/reply)設(shè)計(jì)時(shí)總是會(huì)很慎重。
如果后端服務(wù)選擇的是HTTP API(rest api),比如json over http,API響應(yīng)(Response)中大多會(huì)包含如下信息:
{
"code": 0,
"msg": "ok",
"payload" : {
... ...
}
}
在這個(gè)http api的響應(yīng)設(shè)計(jì)中,前兩個(gè)狀態(tài)標(biāo)識(shí)這個(gè)請(qǐng)求的響應(yīng)狀態(tài)。這個(gè)狀態(tài)由一個(gè)狀態(tài)代碼(code)與狀態(tài)信息(msg)組成。狀態(tài)信息是對(duì)狀態(tài)代碼所對(duì)應(yīng)錯(cuò)誤原因的詳細(xì)詮釋。只有當(dāng)狀態(tài)為正常時(shí)(code = 0),后面的payload才具有意義。payload顯然是在響應(yīng)中意圖傳給客戶端的業(yè)務(wù)信息。
這樣的服務(wù)響應(yīng)設(shè)計(jì)是目前比較常用且成熟的方案,理解起來也十分容易。
好,現(xiàn)在我們看看另外一大類服務(wù):采用RPC方式提供的服務(wù)。我們還是以使用最為廣泛的gRPC為例。在gRPC中,一個(gè)service的定義如下(我們借用一下grpc-go提供的helloworld示例):
// https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.proto
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
grpc對(duì)于每個(gè)rpc方法(比如SayHello)都有約束,只能有一個(gè)輸入?yún)?shù)和一個(gè)返回值。這個(gè).proto定義通過protoc生成的go代碼變成了這樣:
// https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld_grpc.pb.go
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
... ...
}
我們看到對(duì)于SayHello RPC方法,protoc生成的go代碼中,SayHello方法的返回值列表中多了一個(gè)Gopher們熟悉的error返回值。對(duì)于已經(jīng)習(xí)慣了HTTP API那套響應(yīng)設(shè)計(jì)的gopher來說,現(xiàn)在問題來了! http api響應(yīng)中表示響應(yīng)狀態(tài)的code與msg究竟是定義在HelloReply這個(gè)業(yè)務(wù)響應(yīng)數(shù)據(jù)中,還是通過error來返回的呢?這個(gè)grpc官方文檔似乎也沒有明確說明(如果各位看官找到位置,可以告訴我哦)。
2. gRPC服務(wù)端響應(yīng)設(shè)計(jì)思路
我們先不急著下結(jié)論!我們繼續(xù)借用helloworld這個(gè)示例程序來測試一下當(dāng)error返回值不為nil時(shí)客戶端的反應(yīng)!先改一下greeter_server的代碼:
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, errors.New("test grpc error")
}
在上面代碼中,我們故意構(gòu)造一個(gè)錯(cuò)誤并返回給調(diào)用該方法的客戶端。我們來運(yùn)行一下這個(gè)服務(wù)并啟動(dòng)greeter_client來訪問該服務(wù),在客戶端側(cè),我們得到的結(jié)果如下:
2021/09/20 17:04:35 could not greet: rpc error: code = Unknown desc = test grpc error
從客戶端的輸出結(jié)果中,我們看到了我們自定義的錯(cuò)誤的內(nèi)容(test grpc error)。但我們還發(fā)現(xiàn)錯(cuò)誤輸出的內(nèi)容中還有一個(gè)”code = Unknown”的輸出,這個(gè)code是從何而來呢?似乎grpc期待的error形式是包含code與desc的形式。
這時(shí)候就不得不查看一下gprc-go(v1.40.0)的參考文檔了!在grpc-go的文檔中我們發(fā)現(xiàn)幾個(gè)被DEPRECATED的與Error有關(guān)的函數(shù):

在這幾個(gè)作廢的函數(shù)的文檔中都提到了用status包的同名函數(shù)替代。那么這個(gè)status包又是何方神圣?我們翻看grpc-go的源碼,終于找到了status包,在包說明的第一句中我們就找到了答案:
Package status implements errors returned by gRPC.
原來status包實(shí)現(xiàn)了上面grpc客戶端所期望的error類型。那么這個(gè)類型是什么樣的呢?我們逐步跟蹤代碼:
在grpc-go/status包中我們看到如下代碼:
type Status = status.Status
// New returns a Status representing c and msg.
func New(c codes.Code, msg string) *Status {
return status.New(c, msg)
}
status包使用了internal/status包中的Status,我們?cè)賮砜磇nternal/status包中Status結(jié)構(gòu)的定義:
// internal/status
type Status struct {
s *spb.Status
}
// New returns a Status representing c and msg.
func New(c codes.Code, msg string) *Status {
return &Status{s: &spb.Status{Code: int32(c), Message: msg}}
}
internal/status包的Status結(jié)構(gòu)體組合了一個(gè)*spb.Status類型(google.golang.org/genproto/googleapis/rpc/status包中的類型)的字段,繼續(xù)追蹤spb.Status:
// https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status
type Status struct {
// The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
// A developer-facing error message, which should be in English. Any
// user-facing error message should be localized and sent in the
// [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
// A list of messages that carry the error details. There is a common set of
// message types for APIs to use.
Details []*anypb.Any `protobuf:"bytes,3,rep,name=details,proto3" json:"details,omitempty"`
// contains filtered or unexported fields
}
我們看到最后的這個(gè)Status結(jié)構(gòu)包含了Code與Message。這樣一來,grpc的設(shè)計(jì)意圖就很明顯了,它期望開發(fā)者在error這個(gè)返回值中包含rpc方法的響應(yīng)狀態(tài),而自定義的響應(yīng)結(jié)構(gòu)體只需包含業(yè)務(wù)所需要的數(shù)據(jù)即可。我們用一幅示意圖來橫向建立一下http api與rpc響應(yīng)的映射關(guān)系:

有了這幅圖,再面對(duì)如何設(shè)計(jì)grpc方法響應(yīng)這個(gè)問題時(shí),我們就胸有成竹了!
grpc-go在codes包中定義了grpc規(guī)范要求的10余種錯(cuò)誤碼:
const (
// OK is returned on success.
OK Code = 0
// Canceled indicates the operation was canceled (typically by the caller).
//
// The gRPC framework will generate this error code when cancellation
// is requested.
Canceled Code = 1
// Unknown error. An example of where this error may be returned is
// if a Status value received from another address space belongs to
// an error-space that is not known in this address space. Also
// errors raised by APIs that do not return enough error information
// may be converted to this error.
//
// The gRPC framework will generate this error code in the above two
// mentioned cases.
Unknown Code = 2
// InvalidArgument indicates client specified an invalid argument.
// Note that this differs from FailedPrecondition. It indicates arguments
// that are problematic regardless of the state of the system
// (e.g., a malformed file name).
//
// This error code will not be generated by the gRPC framework.
InvalidArgument Code = 3
... ...
// Unauthenticated indicates the request does not have valid
// authentication credentials for the operation.
//
// The gRPC framework will generate this error code when the
// authentication metadata is invalid or a Credentials callback fails,
// but also expect authentication middleware to generate it.
Unauthenticated Code = 16
在這些標(biāo)準(zhǔn)錯(cuò)誤碼之外,我們還可以擴(kuò)展定義自己的錯(cuò)誤碼與錯(cuò)誤描述。
3. 服務(wù)端如何構(gòu)造error與客戶端如何解析error
前面提到,gRPC服務(wù)端采用rpc方法的最后一個(gè)返回值error來承載應(yīng)答狀態(tài)。google.golang.org/grpc/status包為構(gòu)建客戶端可解析的error提供了一些方便的函數(shù),我們看下面示例(基于上面helloworld的greeter_server改造):
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return nil, status.Errorf(codes.InvalidArgument, "you have a wrong name: %s", in.GetName())
}
status包提供了一個(gè)類似于fmt.Errorf的函數(shù),我們可以很方便的構(gòu)造一個(gè)帶有code與msg的error實(shí)例并返回給客戶端。
而客戶端同樣可以通過status包提供的函數(shù)將error中攜帶的信息解析出來,我們看下面代碼:
ctx, _ := context.WithTimeout(context.Background(), time.Second)
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "tony")})
if err != nil {
errStatus := status.Convert(err)
log.Printf("SayHello return error: code: %d, msg: %s\n", errStatus.Code(), errStatus.Message())
}
log.Printf("Greeting: %s", r.GetMessage())
我們看到:通過status.Convert函數(shù)可以很簡答地將rpc方法返回的不為nil的error中攜帶的信息提取出來。
4. 空應(yīng)答
gRPC的proto文件規(guī)范要求每個(gè)rpc方法的定義中都必須包含一個(gè)返回值,返回值不能為空,比如上面helloworld項(xiàng)目的.proto文件中的SayHello方法:
rpc SayHello (HelloRequest) returns (HelloReply) {}
如果去掉HelloReply這個(gè)返回值,那么protoc在生成代碼時(shí)會(huì)報(bào)錯(cuò)!
但是有些方法本身不需要返回業(yè)務(wù)數(shù)據(jù),那么我們就需要為其定義一個(gè)空應(yīng)答消息,比如:
message Empty {
}
考慮到每個(gè)項(xiàng)目在遇到空應(yīng)答時(shí)都要重復(fù)造上面Empty message定義的輪子,grpc官方提供了一個(gè)可被復(fù)用的空message:
// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/empty.proto
// A generic empty message that you can re-use to avoid defining duplicated
// empty messages in your APIs. A typical example is to use it as the request
// or the response type of an API method. For instance:
//
// service Foo {
// rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
// }
//
// The JSON representation for `Empty` is empty JSON object `{}`.
message Empty {}
我們只需在.proto文件中導(dǎo)入該empty.proto并使用Empty即可,比如下面代碼:
// xxx.proto
syntax = "proto3";
import "google/protobuf/empty.proto";
service MyService {
rpc MyRPCMethod(...) returns (google.protobuf.Empty);
}
當(dāng)然google.protobuf.Empty不僅僅適用于空響應(yīng),也適合空請(qǐng)求,這個(gè)就留給大家可自行完成吧。
5. 小結(jié)
本文我們講述了gRPC服務(wù)端響應(yīng)設(shè)計(jì)的相關(guān)內(nèi)容,最主要想說的是直接使用gRPC生成的rpc方面的error返回值來表示rpc調(diào)用的響應(yīng)狀態(tài),不要再在自定義的Message結(jié)構(gòu)中重復(fù)放入code與msg字段來表示響應(yīng)狀態(tài)了。
btw,做API的錯(cuò)誤設(shè)計(jì),google的這份API設(shè)計(jì)方面的參考資料是十分好的。有時(shí)間一定要好好讀讀哦。