kratos项目通过统一定义common.proto、复用领域模型与错误码,并抽取独立biz层,实现http与grpc共用同一套业务逻辑、参数校验和错误处理。

在Kratos项目中,需让同一套业务逻辑同时支撑HTTP和gRPC两种传输协议,避免重复编写Service方法体、重复校验参数、重复处理错误返回。
统一定义领域模型与错误码
在api/目录下新建common/common.proto,定义共享的message与error enum:
syntax = "proto3";
package kratos.api.common;
import "google/api/annotations.proto";
message Empty {}
message Status {
int32 code = 1;
string message = 2;
}
enum ErrorCode {
UNSPECIFIED = 0;
NOT_FOUND = 404;
INVALID_ARGUMENT = 400;
INTERNAL_ERROR = 500;
}
【必须将common.proto置于api/common/路径下,且package名严格为kratos.api.common】否则后续生成的Go类型无法被其他proto正确import,编译时报错“cannot find package”。
在业务proto中复用公共定义
以api/greeter/greeter.proto为例,直接import并使用:
import "api/common/common.proto";
import "google/api/annotations.proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = { get: "/greeter/hello" };
}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
kratos.api.common.Status status = 2;
}
这一步做完后,执行kratos proto client api/greeter/greeter.proto,生成的pb.go中HelloReply.Status字段类型会自动映射为*common.Status,无需手动转换。
抽取独立的Biz层实现复用逻辑
第一步:在internal/biz/目录下创建greeter.go,定义纯业务结构体与方法:
type GreeterUsecase struct {
repo Repo
}
func (uc *GreeterUsecase) SayHello(ctx context.Context, name string) (string, error) {
if name == "" {
return "", errors.New("name is required")
}
return fmt.Sprintf("Hello %s", name), nil
}
第二步:在internal/service/中分别实现HTTP Handler与gRPC Server,都调用同一Biz层:
// internal/service/greeter_service.go
func (s *GreeterService) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
msg, err := s.uc.SayHello(ctx, req.Name)
if err != nil {
return &pb.HelloReply{Status: &common.Status{Code: 400, Message: err.Error()}}, nil
}
return &pb.HelloReply{Message: msg, Status: &common.Status{Code: 0, Message: "OK"}}, nil
}
// internal/server/http.go 中的 handler
func (s *GreeterService) sayHelloHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
msg, err := s.uc.SayHello(r.Context(), name)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(map[string]string{"message": msg})
}
}
错误码与HTTP状态码自动对齐
方法一:在Biz层返回标准error,由transport层统一转译
在internal/transport/http/middleware/error.go中添加中间件:
func ErrorTranslator() middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req any) (any, error) {
reply, err := handler(ctx, req)
if err != nil {
switch {
case errors.Is(err, biz.ErrNotFound):
return nil, transport.NewError(http.StatusNotFound, err.Error())
case errors.Is(err, biz.ErrInvalidArgument):
return nil, transport.NewError(http.StatusBadRequest, err.Error())
default:
return nil, transport.NewError(http.StatusInternalServerError, "internal error")
}
}
return reply, nil
}
}
}
方法二:gRPC端复用同一套error判定逻辑,在server.go中注册时传入统一的Unmarshaler
srv := grpc.NewServer(
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
grpc_recovery.UnaryServerInterceptor(),
grpc_zap.UnaryServerInterceptor(zapLogger),
biz.ErrorUnaryServerInterceptor(), // 复用biz包里的拦截器
)),
)
这一步做完后,无论HTTP还是gRPC调用,只要Biz层返回biz.ErrInvalidArgument,上层就会自动映射为400或gRPC的StatusCode_INVALID_ARGUMENT,无需在每个Service方法里重复写switch。











