gokit三层结构必须显式拆分:service层专注业务逻辑,endpoint层负责请求响应结构转换,transport层仅处理协议编解码;三者物理隔离是保障可测试性、中间件注入及http/grpc协议切换的基础。

GoKit 的三层结构不是可选,是必须显式拆分
很多人试图把 Service、Endpoint、Transport 塞进一个文件里,结果越写越乱,测试难写、中间件难加、协议切换(HTTP ↔ gRPC)直接重写一半。GoKit 的设计前提是这三层职责必须物理隔离——不是“可以拆”,而是“不拆就废”。
拆分后各层作用非常明确:
-
Service层只管业务逻辑,不碰任何网络、序列化、上下文提取细节;它接收原始参数,返回原始值或 error -
Endpoint层负责“翻译”:把 transport 层的请求结构(比如GetRequest)转成Service能理解的参数,再把返回值包装成GetResponse -
Transport层只干两件事:解码入参(JSON / protobuf)、编码出参,并绑定到具体协议(http.Server或grpc.Server)
典型错误是让 Service 方法接收 *http.Request 或 context.Context 以外的框架类型——这会让单元测试被迫启动 HTTP server,也堵死了 gRPC 复用同一套业务逻辑的路。
Endpoint 必须为每个 Service 方法单独定义 request/response 结构体
别图省事复用同一个 struct,也别用 map 或 interface{} 接收参数。GoKit 的 Endpoint 类型签名是 func(ctx context.Context, request interface{}) (response interface{}, err error),它的泛型能力靠的是你明确定义的结构体。
例如 Get(ctx, key string) (string, error) 这个 Service 方法,对应 Endpoint 必须有:
type GetRequest struct {
Key string `json:"key"`
}
type GetResponse struct {
Value string `json:"value"`
Err string `json:"err,omitempty"`
}
这样做的关键原因:
- HTTP 和 gRPC 的字段映射规则不同(gRPC 用 proto 字段编号,HTTP 用 JSON tag),共用结构体极易踩坑
- 中间件(如限流、日志)依赖
request和response的稳定结构做采样或脱敏,动态类型无法保证 - 生成 OpenAPI 或 proto 文件时,结构体名和字段是唯一可信来源
Transport 层不能直接调用 Service,必须经由 Endpoint
常见错误写法:http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { val, _ := svc.Get(r.Context(), r.URL.Query().Get("key")) ... })。这绕过了整个 GoKit 生态,等于放弃熔断、指标、追踪、日志装饰等所有中间件能力。
正确链路必须是:HTTP handler → decode → Endpoint → middleware stack → Service → encode → response。其中 kittransport.NewServer 就是这个链路的 glue:
getHandler := kittransport.NewServer(
makeGetEndpoint(svc), // ← 这个才是你的业务入口
decodeGetRequest,
encodeGetResponse,
kittransport.ServerBefore(extractContext),
)
注意点:
-
makeGetEndpoint返回的是endpoint.Endpoint类型,不是函数指针也不是方法值 -
decodeGetRequest必须返回GetRequest实例,不能返回map[string]string—— 否则后续中间件拿不到结构化字段 - 如果同时支持 HTTP 和 gRPC,
makeGetEndpoint可复用,但decode/encode函数必须各自实现
Service 接口的 error 处理必须收敛到顶层
GoKit 不处理 error 的语义,只传递。但你得决定:哪些 error 是业务失败(如 key not found),哪些是系统错误(如 DB timeout)。前者应转成 HTTP 404 或 gRPC codes.NotFound,后者应触发熔断或重试。
推荐做法是在 Endpoint 层统一分类:
- 用自定义 error 类型(如
ErrNotFound、ErrInvalidInput)标记业务错误,encodeResponse中检查并设 status code - 让所有中间件(日志、指标)只记录原始 error,不 panic、不 recover —— GoKit 本身不假设你如何处理 error
- 避免在
Service层做log.Error()或metrics.Inc(),这些属于 transport 或 endpoint 层关注点
最易被忽略的是:gRPC 的 error 编码必须用 status.Errorf,否则客户端拿到的是 generic error,无法做 status.Code(err) == codes.NotFound 判断。











