tRPC-Go 框架 03:服务端开发——Handler、Filter 与错误处理
tRPC-Go 框架 03服务端开发——Handler、Filter 与错误处理掌握了 Hello World 后本篇深入讲解 tRPC-Go 服务端的核心开发模式handler 怎么写、filter 怎么用、错误怎么传。一、Handler 的本质trpc create给我们生成的 service 接口长这样typeGreeterServiceinterface{Hello(ctx context.Context,req*HelloReq)(*HelloRsp,error)Bye(ctx context.Context,req*ByeReq)(*ByeRsp,error)}只要实现这个接口并通过pb.RegisterGreeterService(s, impl)注册框架就会负责监听端口拆包解码路由分发调用你的 handler编码响应发包。1.1 多 method 共用 structtypegreeterImplstruct{repo UserRepo cfg*Config}func(g*greeterImpl)Hello(ctx context.Context,req*pb.HelloReq)(*pb.HelloRsp,error){user,err:g.repo.Get(ctx,req.Id)iferr!nil{returnnil,err}returnpb.HelloRsp{Msg:Hello user.Name},nil}func(g*greeterImpl)Bye(ctx context.Context,req*pb.ByeReq)(*pb.ByeRsp,error){returnpb.ByeRsp{Msg:Bye},nil}依赖通过构造函数注入便于测试。1.2 在 main 中组装funcmain(){s:trpc.NewServer()repo:repository.NewUserRepo(db)impl:greeterImpl{repo:repo,cfg:cfg}pb.RegisterGreeterService(s,impl)log.Fatal(s.Serve())}二、context 的妙用ctx在 tRPC 中承载了海量信息importtrpc.group/trpc-go/trpc-gomsg:trpc.Message(ctx)// 取出当前 RPC 消息log.Printf(caller%s,msg.CallerServiceName())log.Printf(callee%s,msg.CalleeServiceName())log.Printf(method%s,msg.CalleeMethod())// 透传字段trans-infomsg.WithServerMetaData(map[string][]byte{trace-id:[]byte(xxx),})2.1 取超时 / 截止时间deadline,ok:ctx.Deadline()ifoktime.Until(deadline)100*time.Millisecond{// 时间不够直接返回}2.2 写日志关联请求tRPC 的log包默认从 ctx 取 trace-idlog.WithContextFields(ctx,uid,uid).Infof(login success)2.3 透传到下游ctx,msg:codec.WithCloneMessage(ctx)msg.WithClientMetaData(map[string][]byte{x-channel:[]byte(ios),})resp,err:downstream.Call(ctx,req)三、Filter拦截器3.1 Filter 的两种类型// 服务端typeServerFilterfunc(ctx context.Context,req any,next ServerHandleFunc)(rsp any,errerror)typeServerHandleFuncfunc(ctx context.Context,req any)(rsp any,errerror)// 客户端typeClientFilterfunc(ctx context.Context,req,rsp any,next ClientHandleFunc)error注意tRPC-Go 不同版本的 filter 签名略有差异请以你使用的版本为准。3.2 写一个简单的 access log filterfuncaccessLogFilter(ctx context.Context,req any,next filter.ServerHandleFunc)(any,error){start:time.Now()msg:trpc.Message(ctx)rsp,err:next(ctx,req)log.Infof([ACCESS] method%s caller%s cost%v err%v,msg.CalleeMethod(),msg.CallerServiceName(),time.Since(start),err)returnrsp,err}注册filter.Register(access_log,accessLogFilter,nil)启用server:filter:[access_log]3.3 内置常用 filterfilter作用recovery捕获 panic 防止崩溃debuglog调试日志validation自动 validator 校验tjg/opentelemetry链路追踪m007/prometheus监控埋点circuitbreaker熔断ratelimit限流强烈建议服务端配置recovery作为第一个 filter避免业务 panic 拖垮整个进程。server:filter:[recovery,debuglog,opentelemetry]3.4 Filter 顺序请求方向filter1 → filter2 → handler → filter2 → filter1响应方向写 filter 时记得next调用前后分别处理。3.5 选择性应用 filter某 method 不需要某些 filterserver:service:-name:trpc.app.public.Hellomethods:-name:HealthCheckfilter:[]# 健康检查接口跳过所有四、错误处理4.1 框架错误模型tRPC 错误用errs.Error结构表示typeErrorstruct{Typeint// 错误类型框架/业务Codeint// 错误码Msgstring// 错误信息Descstring// 描述}4.2 业务错误importtrpc.group/trpc-go/trpc-go/errsfunc(s*svc)GetUser(ctx context.Context,req*pb.GetUserReq)(*pb.GetUserRsp,error){ifreq.Id0{returnnil,errs.New(10001,invalid id)}user,err:s.repo.Get(ctx,req.Id)iferrors.Is(err,sql.ErrNoRows){returnnil,errs.New(10404,user not found)}iferr!nil{returnnil,errs.Wrap(err,10500,internal error)}returntoRsp(user),nil}4.3 错误码规范建议1xxx 通用错误参数、鉴权… 1xxxx 用户中心错误 2xxxx 订单服务错误 ... 框架码 0 或 100000保留给框架4.4 错误如何被对端拿到服务端 return 的errs.Error会被 tRPC 协议头携带客户端调用rsp,err:proxy.GetUser(ctx,req)iferr!nil{code:errs.Code(err)msg:errs.Msg(err)ifcode10404{// not found 处理}}errs.Code(err)是常用的提取方式。4.5 别忘了 panic recover服务端 panic 会被recoveryfilter 转化为errs.Errorfilter.Register(recovery,func(ctx context.Context,req any,next filter.ServerHandleFunc)(rsp any,errerror){deferfunc(){ifr:recover();r!nil{log.Errorf(panic recovered: %v\n%s,r,debug.Stack())errerrs.New(int(errs.RetServerSystemErr),fmt.Sprintf(panic: %v,r))}}()returnnext(ctx,req)},nil)五、参数校验可使用protoc-gen-validate在 proto 中声明import validate/validate.proto; message CreateUserReq { string name 1 [(validate.rules).string {min_len: 1, max_len: 50}]; int32 age 2 [(validate.rules).int32 {gte: 0, lte: 150}]; string email 3 [(validate.rules).string.email true]; }启用validationfilter 后非法请求自动被拦截无需手写校验。六、并发与性能每个请求都在独立 goroutine 中处理所以业务代码可放心调用阻塞 IO共享资源需自行加锁避免for内启 goroutine 不带退出机制。func(s*svc)Heavy(ctx context.Context,req*pb.HeavyReq)(*pb.HeavyRsp,error){varwg sync.WaitGroup results:make([]Result,len(req.Items))fori,item:rangereq.Items{wg.Add(1)gofunc(iint,item*Item){deferwg.Done()results[i]process(ctx,item)}(i,item)}wg.Wait()returnpb.HeavyRsp{Results:results},nil}七、优雅关闭tRPC 默认支持 SIGTERM/SIGINT 优雅退出停止接收新请求等待 inflight 请求处理完执行 close 钩子。注册退出钩子trpc.GoAndWait(func()error{-ctx.Done()log.Info(server stopping...)db.Close()returnnil})或s:trpc.NewServer()s.RegisterOnShutdown(func(){log.Info(clean up resources)db.Close()})八、健康检查tRPC 不强制实现但社区惯例是开放一个/healthz或专门的 service 方法service Health { rpc Check(HealthCheckReq) returns (HealthCheckRsp); }func(s*health)Check(ctx context.Context,req*pb.HealthCheckReq)(*pb.HealthCheckRsp,error){if!s.db.Ping(){returnnil,errs.New(503,db unavailable)}returnpb.HealthCheckRsp{Status:OK},nil}九、实战完整的服务端骨架funcmain(){// 1. 启动配置 框架s:trpc.NewServer()// 2. 资源初始化db:mustInitDB()deferdb.Close()cache:mustInitRedis()// 3. 业务实例repo:repository.NewUserRepo(db,cache)impl:service.NewUserService(repo)// 4. 注册 servicepb.RegisterUserService(s,impl)// 5. 启动iferr:s.Serve();err!nil{log.Fatal(err)}}业务方法func(s*UserService)GetUser(ctx context.Context,req*pb.GetUserReq)(*pb.GetUserRsp,error){ifreq.Id0{returnnil,errs.New(10001,invalid id)}u,err:s.repo.GetByID(ctx,req.Id)iferr!nil{iferrors.Is(err,repository.ErrNotFound){returnnil,errs.New(10404,user not found)}log.WithContext(ctx).Errorf(get user fail: %v,err)returnnil,errs.Wrap(err,10500,internal error)}returnpb.GetUserRsp{User:toPb(u)},nil}十、小结handler 是普通的 Go 方法关心业务即可ctx 承载了 RPC 元信息和透传字段filter 是 tRPC 的切面机制必备recovery错误用errs.New/Wrap错误码体系要规范利用 protoc-gen-validate 自动参数校验优雅关闭、健康检查是生产必备。下一篇我们将站到调用方视角讲解客户端开发proxy、selector、超时与重试。