tRPC-Go 框架 04客户端开发——Proxy、Selector、超时与重试服务调用是分布式系统的常态。本篇从调用方视角出发讲清楚 tRPC-Go 客户端的开发模式ClientProxy 怎么用、target 怎么配、超时重试怎么加、并发调用怎么写。一、ClientProxytrpc create生成的客户端骨架typeGreeterClientProxyinterface{Hello(ctx context.Context,req*HelloReq,opts...client.Option)(*HelloRsp,error)}funcNewGreeterClientProxy(opts...client.Option)GreeterClientProxy{...}最朴素调用proxy:pb.NewGreeterClientProxy(client.WithTarget(ip://127.0.0.1:8000),)rsp,err:proxy.Hello(ctx,pb.HelloReq{Msg:World})注意opts在New与Hello中都可传后者会覆盖前者——常用于按调用粒度调超时/重试。二、Target寻址语法target 决定调谁schema://service?keyvaluekeyvalueschema用途示例ip直连ip://127.0.0.1:8000dnsDNSdns://api.example.com:80polaris北极星polaris://trpc.app.usercl5腾讯 CL5cl5://12345:67890consul/etcd自定义通过插件注册参数示例polaris://trpc.app.user?namespaceProductionversionv2target 可以放在配置文件里client:service:-name:trpc.app.user.Usertarget:polaris://trpc.app.user.Userprotocol:trpctimeout:1000filter:[retry,debuglog]代码中只需proxy:pb.NewUserClientProxy()// 自动从配置中加载 target/protocol/timeout/filter生产推荐调用参数尽量走配置代码只调用部署时改 yaml。三、协议tRPC 客户端可选多种协议client.WithProtocol(trpc)// 默认client.WithProtocol(http)client.WithProtocol(grpc)切到 HTTP 时还可以用 JSONclient.WithSerializationType(codec.SerializationTypeJSON)四、超时设置超时有三个层级优先级从高到低单次调用 opt Service 级配置 全局默认// 1. 全局client.DefaultClientclient.New()// 默认 1s// 2. 配置文件service:timeout:1000// 3. 单次proxy.Hello(ctx,req,client.WithTimeout(500*time.Millisecond))配合contextctx,cancel:context.WithTimeout(ctx,500*time.Millisecond)defercancel()proxy.Hello(ctx,req)ctx 的 deadline 会与 client 配置中取较小值。五、重试启用retryfilterclient:filter:[retry]service:-name:trpc.app.userretry:2或代码注册filter.Register(retry,retryFilter,nil)实际生产中更推荐自定义重试 filterfuncretryFilter(maxRetryint)filter.ClientFilter{returnfunc(ctx context.Context,req,rsp any,next filter.ClientHandleFunc)error{varerrerrorfori:0;imaxRetry;i{errnext(ctx,req,rsp)iferrnil{returnnil}if!shouldRetry(err)||ctx.Err()!nil{returnerr}time.Sleep(backoff(i))}returnerr}}funcshouldRetry(errerror)bool{code:errs.Code(err)// 仅对网络/超时错误重试returncodeerrs.RetClientNetErr||codeerrs.RetClientTimeout}funcbackoff(nint)time.Duration{base:50*time.Millisecond max:2*time.Second d:basenifdmax{dmax}jitter:time.Duration(rand.Int63n(int64(d)/4))returndjitter}重要写操作非幂等时禁止重试。六、负载均衡选择client:service:-name:trpc.app.usertarget:polaris://trpc.app.userloadbalance:weighted_random# weighted_random / round_robin / consistent_hash / p2c或代码proxy.Hello(ctx,req,client.WithBalancerName(p2c))按需用一致性哈希proxy.Hello(ctx,req,client.WithBalancerName(consistent_hash),client.WithKey(userID),)七、节点过滤与路由通过 metadata 选定一组节点proxy.Hello(ctx,req,client.WithCalleeSetName(set.shanghai.1),client.WithCalleeContainerName(prod),)或在 Polaris 中配置路由规则按header.x-uin走特定 set。八、并发调用8.1 errgroup 并发批量调用importgolang.org/x/sync/errgroupfunc(s*svc)Aggregate(ctx context.Context,ids[]int64)([]*User,error){users:make([]*User,len(ids))g,gctx:errgroup.WithContext(ctx)fori,id:rangeids{i,id:i,id g.Go(func()error{rsp,err:s.userProxy.GetUser(gctx,pb.GetUserReq{Id:id})iferr!nil{returnerr}users[i]rsp.Userreturnnil})}iferr:g.Wait();err!nil{returnnil,err}returnusers,nil}8.2 限制并发数sem:make(chanstruct{},10)// 最多 10 并发for_,id:rangeids{sem-struct{}{}gofunc(idint64){deferfunc(){-sem}()proxy.GetUser(ctx,pb.GetUserReq{Id:id})}(id)}8.3 部分失败容忍g,gctx:errgroup.WithContext(ctx)g.SetLimit(10)results:make([]*User,len(ids))fori,id:rangeids{i,id:i,id g.Go(func()error{rsp,err:s.userProxy.GetUser(gctx,req)iferr!nil{log.WithContext(gctx).Warnf(get user %d failed: %v,id,err)returnnil// 容忍单个失败}results[i]rsp.Userreturnnil})}g.Wait()九、透传字段trans-info调用时传 metadataproxy.Hello(ctx,req,client.WithMetaData(x-trace-id,[]byte(xxxxxx)),client.WithMetaData(x-channel,[]byte(ios)),)服务端读取msg:trpc.Message(ctx)md:msg.ServerMetaData()trace:md[x-trace-id]十、Mock 测试trpc create自动生成的helloworld_mock.go可在测试中替换 proxyfuncTestSvc_DoSomething(t*testing.T){ctrl:gomock.NewController(t)mockUser:pb.NewMockUserClientProxy(ctrl)mockUser.EXPECT().GetUser(gomock.Any(),pb.GetUserReq{Id:1}).Return(pb.GetUserRsp{User:pb.User{Name:Tom}},nil)s:NewMyService(mockUser)rsp,err:s.DoSomething(context.Background(),1)assert.NoError(t,err)assert.Equal(t,Tom,rsp.Name)}十一、调用其他协议HTTP / gRPC11.1 调用 HTTPJSONproxy:pb.NewUserClientProxy(client.WithTarget(ip://api.example.com),client.WithProtocol(http),client.WithSerializationType(codec.SerializationTypeJSON),)11.2 调用 gRPCproxy:pb.NewUserClientProxy(client.WithTarget(ip://127.0.0.1:9090),client.WithProtocol(grpc),)服务端只要也是grpc协议或 gRPC 服务即可互通。十二、连接池tRPC 默认按target复用连接池参数可调client:service:-name:trpc.app.userconn_type:long# long(默认) / shortmax_idle:100idle_timeout:60s短连接极少使用除非访问外部不可控网关。十三、与服务端开发的对照表概念服务端客户端入口s.Serve()proxy.Method(ctx, req)Filterserver.filterclient.filter错误returnerrs.Errorerrs.Code(err)解析超时配置接收上限配置发起上限LB不关心loadbalance配置十四、客户端的自我修养在生产环境使用 tRPC 客户端请永远做到✅ 设置合理超时永远不要无限等待✅ 重试只用于幂等接口✅ 启用 debuglog开发期/ 监控 filter生产期✅ 不要每次调用都Newproxy连接池缓存✅ context 透传贯穿全链路trace-id、超时、取消信号。十五、小结ClientProxy 是与服务端约定的契约接口target 用schema://service?args寻址超时三层级调用 配置 默认重试要看错误类型 幂等性 退避mock 让单测彻底脱离网络。下一篇是 tRPC-Go 系列收官生产级实践——配置、日志、监控、可观测性。
tRPC-Go 框架 04:客户端开发——Proxy、Selector、超时与重试
发布时间:2026/5/21 17:03:32
tRPC-Go 框架 04客户端开发——Proxy、Selector、超时与重试服务调用是分布式系统的常态。本篇从调用方视角出发讲清楚 tRPC-Go 客户端的开发模式ClientProxy 怎么用、target 怎么配、超时重试怎么加、并发调用怎么写。一、ClientProxytrpc create生成的客户端骨架typeGreeterClientProxyinterface{Hello(ctx context.Context,req*HelloReq,opts...client.Option)(*HelloRsp,error)}funcNewGreeterClientProxy(opts...client.Option)GreeterClientProxy{...}最朴素调用proxy:pb.NewGreeterClientProxy(client.WithTarget(ip://127.0.0.1:8000),)rsp,err:proxy.Hello(ctx,pb.HelloReq{Msg:World})注意opts在New与Hello中都可传后者会覆盖前者——常用于按调用粒度调超时/重试。二、Target寻址语法target 决定调谁schema://service?keyvaluekeyvalueschema用途示例ip直连ip://127.0.0.1:8000dnsDNSdns://api.example.com:80polaris北极星polaris://trpc.app.usercl5腾讯 CL5cl5://12345:67890consul/etcd自定义通过插件注册参数示例polaris://trpc.app.user?namespaceProductionversionv2target 可以放在配置文件里client:service:-name:trpc.app.user.Usertarget:polaris://trpc.app.user.Userprotocol:trpctimeout:1000filter:[retry,debuglog]代码中只需proxy:pb.NewUserClientProxy()// 自动从配置中加载 target/protocol/timeout/filter生产推荐调用参数尽量走配置代码只调用部署时改 yaml。三、协议tRPC 客户端可选多种协议client.WithProtocol(trpc)// 默认client.WithProtocol(http)client.WithProtocol(grpc)切到 HTTP 时还可以用 JSONclient.WithSerializationType(codec.SerializationTypeJSON)四、超时设置超时有三个层级优先级从高到低单次调用 opt Service 级配置 全局默认// 1. 全局client.DefaultClientclient.New()// 默认 1s// 2. 配置文件service:timeout:1000// 3. 单次proxy.Hello(ctx,req,client.WithTimeout(500*time.Millisecond))配合contextctx,cancel:context.WithTimeout(ctx,500*time.Millisecond)defercancel()proxy.Hello(ctx,req)ctx 的 deadline 会与 client 配置中取较小值。五、重试启用retryfilterclient:filter:[retry]service:-name:trpc.app.userretry:2或代码注册filter.Register(retry,retryFilter,nil)实际生产中更推荐自定义重试 filterfuncretryFilter(maxRetryint)filter.ClientFilter{returnfunc(ctx context.Context,req,rsp any,next filter.ClientHandleFunc)error{varerrerrorfori:0;imaxRetry;i{errnext(ctx,req,rsp)iferrnil{returnnil}if!shouldRetry(err)||ctx.Err()!nil{returnerr}time.Sleep(backoff(i))}returnerr}}funcshouldRetry(errerror)bool{code:errs.Code(err)// 仅对网络/超时错误重试returncodeerrs.RetClientNetErr||codeerrs.RetClientTimeout}funcbackoff(nint)time.Duration{base:50*time.Millisecond max:2*time.Second d:basenifdmax{dmax}jitter:time.Duration(rand.Int63n(int64(d)/4))returndjitter}重要写操作非幂等时禁止重试。六、负载均衡选择client:service:-name:trpc.app.usertarget:polaris://trpc.app.userloadbalance:weighted_random# weighted_random / round_robin / consistent_hash / p2c或代码proxy.Hello(ctx,req,client.WithBalancerName(p2c))按需用一致性哈希proxy.Hello(ctx,req,client.WithBalancerName(consistent_hash),client.WithKey(userID),)七、节点过滤与路由通过 metadata 选定一组节点proxy.Hello(ctx,req,client.WithCalleeSetName(set.shanghai.1),client.WithCalleeContainerName(prod),)或在 Polaris 中配置路由规则按header.x-uin走特定 set。八、并发调用8.1 errgroup 并发批量调用importgolang.org/x/sync/errgroupfunc(s*svc)Aggregate(ctx context.Context,ids[]int64)([]*User,error){users:make([]*User,len(ids))g,gctx:errgroup.WithContext(ctx)fori,id:rangeids{i,id:i,id g.Go(func()error{rsp,err:s.userProxy.GetUser(gctx,pb.GetUserReq{Id:id})iferr!nil{returnerr}users[i]rsp.Userreturnnil})}iferr:g.Wait();err!nil{returnnil,err}returnusers,nil}8.2 限制并发数sem:make(chanstruct{},10)// 最多 10 并发for_,id:rangeids{sem-struct{}{}gofunc(idint64){deferfunc(){-sem}()proxy.GetUser(ctx,pb.GetUserReq{Id:id})}(id)}8.3 部分失败容忍g,gctx:errgroup.WithContext(ctx)g.SetLimit(10)results:make([]*User,len(ids))fori,id:rangeids{i,id:i,id g.Go(func()error{rsp,err:s.userProxy.GetUser(gctx,req)iferr!nil{log.WithContext(gctx).Warnf(get user %d failed: %v,id,err)returnnil// 容忍单个失败}results[i]rsp.Userreturnnil})}g.Wait()九、透传字段trans-info调用时传 metadataproxy.Hello(ctx,req,client.WithMetaData(x-trace-id,[]byte(xxxxxx)),client.WithMetaData(x-channel,[]byte(ios)),)服务端读取msg:trpc.Message(ctx)md:msg.ServerMetaData()trace:md[x-trace-id]十、Mock 测试trpc create自动生成的helloworld_mock.go可在测试中替换 proxyfuncTestSvc_DoSomething(t*testing.T){ctrl:gomock.NewController(t)mockUser:pb.NewMockUserClientProxy(ctrl)mockUser.EXPECT().GetUser(gomock.Any(),pb.GetUserReq{Id:1}).Return(pb.GetUserRsp{User:pb.User{Name:Tom}},nil)s:NewMyService(mockUser)rsp,err:s.DoSomething(context.Background(),1)assert.NoError(t,err)assert.Equal(t,Tom,rsp.Name)}十一、调用其他协议HTTP / gRPC11.1 调用 HTTPJSONproxy:pb.NewUserClientProxy(client.WithTarget(ip://api.example.com),client.WithProtocol(http),client.WithSerializationType(codec.SerializationTypeJSON),)11.2 调用 gRPCproxy:pb.NewUserClientProxy(client.WithTarget(ip://127.0.0.1:9090),client.WithProtocol(grpc),)服务端只要也是grpc协议或 gRPC 服务即可互通。十二、连接池tRPC 默认按target复用连接池参数可调client:service:-name:trpc.app.userconn_type:long# long(默认) / shortmax_idle:100idle_timeout:60s短连接极少使用除非访问外部不可控网关。十三、与服务端开发的对照表概念服务端客户端入口s.Serve()proxy.Method(ctx, req)Filterserver.filterclient.filter错误returnerrs.Errorerrs.Code(err)解析超时配置接收上限配置发起上限LB不关心loadbalance配置十四、客户端的自我修养在生产环境使用 tRPC 客户端请永远做到✅ 设置合理超时永远不要无限等待✅ 重试只用于幂等接口✅ 启用 debuglog开发期/ 监控 filter生产期✅ 不要每次调用都Newproxy连接池缓存✅ context 透传贯穿全链路trace-id、超时、取消信号。十五、小结ClientProxy 是与服务端约定的契约接口target 用schema://service?args寻址超时三层级调用 配置 默认重试要看错误类型 幂等性 退避mock 让单测彻底脱离网络。下一篇是 tRPC-Go 系列收官生产级实践——配置、日志、监控、可观测性。