☰
Go-Kit JSON-RPC 实战指南:用 EndpointCodec 构建标准 JSON-RPC 2.0 服务
2026/9/30 7:02:16 网站建设 项目流程
  • 微服务
  • 后端
  • RPC框架

【免费下载链接】kit

A standard library for microservices.

项目地址:https://gitcode.com/gh_mirrors/ki/kit
点击查看免费下载

JSON-RPC 是一种"轻量级远程过程调用协议",它以人类可读的 JSON 报文完成跨服务方法调用,天然适合微服务之间或前后端之间的 RPC 风格 API。go-kit 的transport/http/jsonrpc包把 JSON-RPC 2.0 绑定到了标准 go-kit Endpoint 之上:服务端以一个http.Handler承载全部方法,客户端则把远程方法封装成可复用、可组合的endpoint.Endpoint。读完本文,你将掌握用EndpointCodec注册与路由 JSON-RPC 方法、用解码器/编码器处理params与result、定制错误对象与错误码、配置服务端/客户端钩子,以及通过客户端封装直接调用远程 JSON-RPC 服务的完整实战方案。

JSON-RPC 与 go-kit 的结合方式

在 go-kit 的架构中,transport/http/jsonrpc位于 transport/http/jsonrpc 目录,它把 JSON-RPC 2.0 协议封装为一种传输绑定(binding)。从整体设计上看:

  • 服务端是一个 HTTP Handler:JSON-RPC server 实现net/http.Handler,接收所有发往特定 URL(例如/rpc)的 POST 请求。它读取 Request Object 中的method属性,据此把请求路由到对应的处理代码(见 server.go 的Server类型声明)。
  • 每个 JSON-RPC 方法是一个EndpointCodec:一个 go-kitEndpoint(定义于 endpoint/endpoint.go),前后夹着解码器和编码器。解码器从 JSON-RPC 请求的params中拆出领域对象交给 Endpoint;编码器接收 Endpoint 的输出,编码为 JSON-RPC 的result字段。
  • 协议版本与媒体类型固定:包内常量Version = "2.0"、ContentType = "application/json; charset=utf-8",定义于 request_response_types.go。

这一"端点 + 编解码"的抽象意味着:你只关心业务逻辑(Endpoint)、请求形状(Decoder)、响应形状(Encoder),传输层细节全部由 jsonrpc 包接管。

完整示例:构建一个 Add(求和)服务

原文档以"两个整数相加"的服务为例,将其暴露在http://localhost/rpc。对sum方法的请求是发往http://localhost/rpc的 POST,请求体如下:

{ "id": 123, "jsonrpc": "2.0", "method": "sum", "params": { "A": 2, "B": 2 } }

下面按"路由表 → 解码器 → 编码器 → 装配服务端"的次序逐步实现。

1. EndpointCodecMap:方法路由表

服务端把"方法名 → 处理单元"的映射表称为EndpointCodecMap。它的 key 是 JSON-RPC 方法名,value 是EndpointCodec(结构定义见 encode_decode.go)。这里我们把sum方法路由到sumEndpoint及其编解码函数:

jsonrpc.EndpointCodecMap{ "sum": jsonrpc.EndpointCodec{ Endpoint: sumEndpoint, Decode: decodeSumRequest, Encode: encodeSumResponse, }, }

2. Decoder:从 params 提取领域对象

解码器的类型签名是DecodeRequestFunc func(context.Context, json.RawMessage) (request interface{}, err error)(见 encode_decode.go)。注意:它拿到的只是 Request Object 中params属性的原始 JSON,不是整个请求对象,返回值将作为 Endpoint 的输入。对本例,输出应当是SumRequest:

type SumRequest struct { A, B int } func decodeSumRequest(ctx context.Context, msg json.RawMessage) (interface{}, error) { var req SumRequest err := json.Unmarshal(msg, &req) if err != nil { return nil, err } return req, nil }

SumRequest接下来会被传入 Endpoint。端点完成业务计算后,控制权交给编码器。

3. Encoder:把结果编码为 result 字段

编码器类型签名是EncodeResponseFunc func(context.Context, interface{}) (response json.RawMessage, err error)(见 encode_decode.go)。它接收 Endpoint 的输出,构建将写入 Response Object 的result字段的原始 JSON。本例的求和结果是一个普通int:

func encodeSumResponse(ctx context.Context, result interface{}) (json.RawMessage, error) { sum, ok := result.(int) if !ok { return nil, errors.New("result is not an int") } b, err := json.Marshal(sum) if err != nil { return nil, err } return b, nil }

4. 端点的实现

按上述解码器与编码器的约定,sumEndpoint需要接收SumRequest并返回int:

func sumEndpoint(ctx context.Context, request interface{}) (interface{}, error) { sumReq, ok := request.(SumRequest) if !ok { return nil, errors.New("request is not a SumRequest") } return sumReq.A + sumReq.B, nil }

这正是标准 go-kit Endpoint 的形态:func(ctx context.Context, request interface{}) (response interface{}, err error)。

5. 装配 Server 并启动

将 EndpointCodec(解码器 + 端点 + 编码器)组装完成后,用jsonrpc.NewServer构造服务端,再注册到标准库的 HTTP 路由上:

handler := jsonrpc.NewServer(jsonrpc.EndpointCodecMap{ "sum": jsonrpc.EndpointCodec{ Endpoint: sumEndpoint, Decode: decodeSumRequest, Encode: encodeSumResponse, }, }) http.Handle("/rpc", handler) http.ListenAndServe(":80", nil)

NewServer的完整签名与默认行为见 server.go:默认使用DefaultErrorEncoder处理错误、使用log.NewNopLogger()(不记录日志),可通过ServerOption覆盖。

完成上述全部代码后,开篇的示例请求会得到如下响应:

{ "jsonrpc": "2.0", "result": 4 }

服务端源码解析:ServeHTTP 的处理流水线

Server.ServeHTTP(server.go)完整展示了请求的生命周期,这也是理解 jsonrpc 包的关键:

  1. HTTP 方法校验:非 POST 请求直接返回405 must POST(对应测试TestCanRejectNonPostRequest,见 server_test.go)。
  2. 执行ServerBefore钩子:在请求体解码前,对原始http.Request做加工,返回值注入 context。
  3. 解析 Request Object:json.NewDecoder(r.Body).Decode(&req),解析失败则返回ParseError(-32700)错误响应。
  4. 注入上下文:把请求 ID 存入requestIDKey、把方法名存入ContextKeyRequestMethod(见 request_response_types.go),后续编解码器可通过 context 读取。
  5. 执行ServerBeforeCodec钩子:此时 JSON 请求体已解码为Request结构,但方法解码器尚未调用——这是检查 RPC 请求内容的最后机会。
  6. 按 method 查路由表:s.ecm[req.Method]查不到时返回MethodNotFoundError(-32601)。
  7. 调用解码器:ecm.Decode(ctx, req.Params),失败则走错误编码。
  8. 调用 Endpoint:ecm.Endpoint(ctx, reqParams),失败同样走错误编码。
  9. 执行ServerAfter钩子:在响应写入客户端前对http.ResponseWriter做加工。
  10. 调用编码器并写回响应:组装Response{ID, JSONRPC: Version, Result},设置Content-Type后 JSON 编码写出。

值得注意的一点:即便业务出错,HTTP 状态码默认仍是200 OK(见DefaultErrorEncoder中的w.WriteHeader(http.StatusOK)),错误信息通过 JSON-RPCerror对象携带。这一设计与 JSON-RPC 规范"错误是响应对象的一部分"保持一致。

服务端可选项:ServerOption 一览

NewServer的第二个参数是变长的ServerOption,全部定义于 server.go:

选项作用
ServerBefore(...httptransport.RequestFunc)在请求体解码之前对http.Request执行钩子函数,常用于鉴权、提取 Header 等
ServerBeforeCodec(...RequestFunc)JSON 请求体已解码为Request之后、方法解码器调用之前执行,可检查method/id/params等 RPC 内容
ServerAfter(...httptransport.ServerResponseFunc)端点调用之后、任何内容写入客户端之前执行
ServerErrorEncoder(ee httptransport.ErrorEncoder)自定义错误编码器,可自行控制错误格式与 HTTP 状态码
ServerErrorLogger(logger log.Logger)记录非致命错误;默认不记录任何日志(NopLogger)
ServerFinalizer(f httptransport.ServerFinalizerFunc)每次 HTTP 请求结束时执行,适合统计、审计;默认不注册

ServerBeforeCodec与ServerBefore的区别正是 jsonrpc 绑定相对普通 HTTP 传输多出的能力:前者可以访问已解析的 JSON-RPCRequest(类型为RequestFunc func(context.Context, *http.Request, Request) context.Context),而后者只能操作原始 HTTP 请求。

错误处理:Error 对象、标准错误码与自定义编码

jsonrpc 包把 JSON-RPC 规范中的 Error Object 建模为Error结构(error.go):

type Error struct { Code int `json:"code"` Message string `json:"message"` Data interface{} `json:"data,omitempty"` }

标准错误码

包内预定义了 JSON-RPC 2.0 规范的标准错误码常量(error.go):

常量取值含义
ParseError-32700服务端解析 JSON 文本失败
InvalidRequestError-32600收到的 JSON 不是合法的 Request 对象
MethodNotFoundError-32601方法不存在或不可用
InvalidParamsError-32602方法参数非法
InternalError-32603服务端内部错误

每个错误码还配有规范默认消息(errorMessagemap,error.go),可通过ErrorMessage(code)查询;Error.Error()在Message为空时回退到默认消息(error.go)。

默认错误编码器的行为

DefaultErrorEncoder(server.go)负责把 Go error 转成 JSON-RPC 错误响应,其行为规则:

  • 始终设置Content-Type: application/json; charset=utf-8;
  • 若错误实现了httptransport.Headerer,则复制其自定义 Header;
  • 默认使用InternalError(-32603)作为错误码;
  • 若错误实现了ErrorCoder接口(即具有ErrorCode() int方法),则使用其返回的错误码;
  • HTTP 状态码固定为200 OK,错误以 JSON-RPCerror字段返回,并回显请求 ID。

ErrorCoder接口定义于 server.go。包内五种内部错误类型(parseError、invalidRequestError、methodNotFoundError、invalidParamsError、internalError)都已实现该接口(error.go),因此服务端内部错误会自动携带正确的错误码。

自定义错误编码器

业务上若想让特定错误映射到特定 HTTP 状态码,可覆盖ServerErrorEncoder。测试TestServerErrorEncoder(server_test.go)演示了将errors.New("teapot")映射为 HTTP 418(http.StatusTeapot)的用法:

handler := jsonrpc.NewServer( ecm, jsonrpc.ServerErrorEncoder(func(_ context.Context, err error, w http.ResponseWriter) { w.WriteHeader(code(err)) }), )

客户端:把远程方法封装成 Endpoint

除了服务端,jsonrpc 包还提供客户端绑定(client.go),用于调用远程 JSON-RPC 方法。Client封装了目标 URL、方法名与编解码函数,其Endpoint()方法返回一个可直接参与 go-kit 组合的endpoint.Endpoint。

最小客户端用法

u, _ := url.Parse("http://localhost/rpc") client := jsonrpc.NewClient(u, "sum") sumEndpoint := client.Endpoint() // 可像普通 Endpoint 一样使用 resp, err := sumEndpoint(ctx, SumRequest{A: 2, B: 2})

NewClient的默认行为(client.go):

  • 使用http.DefaultClient发起请求;
  • 请求体编码用DefaultRequestEncoder(直接json.Marshal请求对象);
  • 响应解码用DefaultResponseDecoder(若响应含Error则返回该错误,否则把result反序列化为interface{});
  • 请求 ID 由NewAutoIncrementID(0)生成自增整数(基于atomic.AddUint64,见 client.go)。

ClientOption 一览

选项作用
SetClient(c httptransport.HTTPClient)替换底层 HTTP 客户端,默认http.DefaultClient
ClientBefore(...httptransport.RequestFunc)请求发出前对http.Request加工(如加 Header),对应测试见 client_test.go
ClientAfter(...httptransport.ClientResponseFunc)收到响应后、解码前执行,可把响应信息放入 context 供解码器读取
ClientFinalizer(f httptransport.ClientFinalizerFunc)每次请求结束(含出错)时执行,常用于错误日志
ClientRequestEncoder(enc EncodeRequestFunc)自定义请求参数编码
ClientResponseDecoder(dec DecodeResponseFunc)自定义响应解码,可自行决定响应中的error是否上抛
ClientRequestIDGenerator(g RequestIDGenerator)自定义请求 ID 生成器(实现Generate() interface{})
BufferedStream(buffered bool)置为 true 时不关闭响应 Body,便于以缓冲流方式传输大文件

客户端发送的请求体结构由clientRequest定义(client.go):jsonrpc、method、params、id四字段,符合 JSON-RPC 2.0 Request Object 规范。测试TestClientHappyPath(client_test.go)端到端验证了客户端请求在服务端被正确解析:ID、JSONRPC版本、params与发送的请求对象完全一致,且before/after/finalizer钩子均被调用。

测试验证:行为即规范

jsonrpc 包的测试文件为本示例的每一环节提供了可运行的验证依据:

  • 服务端错误路径:TestServerBadDecode、TestServerBadEndpoint、TestServerBadEncode(server_test.go)分别验证解码、端点、编码失败时返回InternalError且保持 HTTP 200、回显请求 ID。
  • 非法请求拒绝:TestCanRejectNonPostRequest验证非 POST 返回 405;TestCanRejectInvalidJSON验证非法 JSON 返回ParseError且 ID 为空(server_test.go)。
  • 未注册方法:TestServerUnregisteredMethod验证未注册方法返回MethodNotFoundError(server_test.go)。
  • 客户端钩子:TestBeforeAfterFuncs验证客户端 before/after/finalizer 在各种响应(空 body、500、错误对象)下均被调用(client_test.go);TestCanUseDefaults验证客户端可完全使用默认编解码。
  • 错误语义:TestError与TestErrorsSatisfyError(error_test.go)验证Error的消息覆盖逻辑、错误消息映射,以及所有内部错误类型都同时满足error与ErrorCoder接口。

小结

go-kit 的 jsonrpc 包把"协议规范"与"业务代码"做了干净切割:服务端只需提供EndpointCodecMap(方法名 → EndpointCodec),客户端只需提供目标 URL 与方法名,即可获得符合 JSON-RPC 2.0 规范的完整通信能力。实际落地时建议:

  1. 用EndpointCodecMap统一管理全部方法路由,保持"一方法一编解码"的清晰映射;
  2. 通过ServerBeforeCodec在解码前统一校验请求内容,通过ServerFinalizer做全量请求审计;
  3. 业务错误实现ErrorCoder接口以携带语义化错误码,必要时用自定义ServerErrorEncoder控制 HTTP 状态;
  4. 客户端侧用ClientResponseDecoder把响应直接解码为领域类型,避免interface{}的层层断言。

掌握了这套 EndpointCodec 模式,你便能在 go-kit 微服务体系中快速接入标准 JSON-RPC 2.0 接口,并让服务端与客户端共享同一套方法语义。

  • 微服务
  • 后端
  • RPC框架

【免费下载链接】kit

A standard library for microservices.

项目地址:https://gitcode.com/gh_mirrors/ki/kit
点击查看免费下载
上一篇:5个实用技巧,让每个人都能轻松保存抖音直播回放
下一篇:Apache APISIX ldap-auth-advanced 插件详解:LDAP 搜索后绑定认证与 Consumer 身份映射实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询