ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

《Go语言高级编程》实战:大型 Web 项目的 CLD 分层架构、多协议支持与代码生成实践

2026/9/20 20:04:13 拓冰建站 浏览量
《Go语言高级编程》实战:大型 Web 项目的 CLD 分层架构、多协议支持与代码生成实践 《Go语言高级编程》实战大型 Web 项目的 CLD 分层架构、多协议支持与代码生成实践【免费下载链接】advanced-go-programming-book:books: 《Go语言高级编程》开源图书涵盖CGO、Go汇编语言、RPC实现、Protobuf插件实现、Web框架实现、分布式系统等高阶主题(完稿)项目地址: https://gitcode.com/gh_mirrors/ad/advanced-go-programming-book大型 Web 项目如何组织代码才能既保持清晰的分层边界、又从容应对多种交互协议并存本文以《Go语言高级编程》开源图书的 ch5-web/ch5-07-layout-of-web-project.md 为核心骨架系统讲解从 MVC 到前后端分离、再到 Controller-Logic-DAOCLD分层与独立 Protocol 层的演进路径并结合仓库中 pbgo 框架、grpc-gateway 与中间件的源码实现深入剖析多协议入口如何通过代码生成来消除重复劳动。读完本文你将掌握一套可直接落地到企业级 Go 后端项目的分层方案与代码生成思路。从 MVC 到前后端分离V 层抽离之后还剩什么流行的 Web 框架大多是 MVC 框架。MVC 概念最早由 Trygve Reenskaug 在 1978 年提出初衷是为了方便扩展 GUI 类型的应用将程序划分为三部分控制器Controller负责转发请求对请求进行处理。视图View界面设计人员进行图形界面设计。模型Model程序员编写程序应有的功能实现算法等数据库专家进行数据管理和数据库设计。随着时代发展前端工程越来越复杂为了更好的工程化现在更流行的是前后端分离架构。可以认为前后端分离就是把 V 层从 MVC 中抽离出来单独成为项目这样一个后端项目一般就只剩下 M 和 C 层。前后端之间通过 ajax 交互跨域问题也已有较为成熟的方案。图 5-13 前后分离交互图图源images/ch6-08-frontend-backend.png图中的 Vue 和 React 是当前前端界流行的两个框架由于本文重点不在前端前端项目内的组织方式不再展开。一个 C 层和一个 M 层不够用纯后端 API 模块的 CLD 分层事实上即使是简单的项目业界也并没有完全遵守 MVC 框架提出者对于 M 和 C 所定义的分工。很多公司的项目会在 Controller 层塞入大量逻辑Model 层只管理数据存储——这往往源于对 model 层字面含义的擅自引申认为这层就是处理某种建模而模型就是数据。这种理解显然有问题业务流程也是一种模型是对真实世界用户行为或既有流程的一种建模并非只有按格式组织的数据才能叫模型。不过按照 MVC 创始人的想法如果和数据打交道的代码加上业务流程全部塞进 M 层这个 M 层又会过于臃肿。对于复杂项目一个 C 和一个 M 层显然不够用当前比较流行的纯后端 API 模块一般采用下述划分方法Controller服务入口负责处理路由、参数校验、请求转发。Logic/Service逻辑服务层是业务逻辑的入口。可以认为从这里开始所有请求参数一定是合法的业务逻辑和业务流程也都在这一层常见设计中会将该层称为Business Rules。DAO/Repository主要负责与数据、存储打交道将下层存储以更简单的函数、接口形式暴露给 Logic 层使用负责数据的持久化工作。每一层都做好自己的工作然后用请求当前的上下文构造下一层工作所需的结构体或其它类型参数然后调用下一层的函数工作完成之后再把处理结果一层层传出到入口。图 5-14 请求处理流程图源images/ch6-08-controller-logic-dao.png这种分层思路与仓库 ch5-web/ch5-08-interface-and-web.md 中用函数封装业务流程、用接口做抽象的方法论一脉相承将ValidateLogin → ValidateParams → AntispamCheck → GetPrice → CreateOrder → UpdateUserStatus → NotifyDownstreamSystems这样的流程逐步拆解为独立步骤函数再在流程稳定后抽象为接口从而让每一层职责单一、可独立演进。引入独立的 Protocol 层一套 CLD多协议并存划分为 CLD 三层之后在 C 层之前我们可能还需要同时支持多种协议。本章前面讲到的 thrift、gRPC 和 http 并不是只能选择其中一种——有时我们需要支持其中两种比如同一个接口既需要效率较高的 thrift也需要方便 debug 的 http 入口。因此除了 CLD 之外还需要一个单独的 protocol 层负责处理各种交互协议的细节。图 5-15 多协议示意图图源images/ch6-08-control-flow.png这样 Controller 中的入口函数就变成下面这样func CreateOrder(ctx context.Context, req *CreateOrderStruct) ( *CreateOrderRespStruct, error, ) { // ... }CreateOrder有两个参数ctx用来传入 trace_id 一类需要串联请求的全局参数req里存储创建订单所需的全部输入信息返回结果是一个响应结构体和错误。可以认为代码运行到 Controller 层之后就没有任何与协议相关的代码了——这里找不到http.Request也找不到http.ResponseWriter更找不到任何与 thrift 或 gRPC 相关的字眼。在协议Protocol层处理 http 协议的代码大概如下// defined in protocol layer type CreateOrderRequest struct { OrderID int64 json:order_id // ... } // defined in controller type CreateOrderParams struct { OrderID int64 } func HTTPCreateOrderHandler(wr http.ResponseWriter, r *http.Request) { var req CreateOrderRequest var params CreateOrderParams ctx : context.TODO() // bind data to req bind(r, req) // map protocol binded to protocol-independent map(req, params) logicResp, err : controller.CreateOrder(ctx, params) if err ! nil {} // ... }理论上可以用同一个请求结构体组合上不同的 tag达到一个结构体给不同协议复用的目的。但遗憾的是在 thrift 中请求结构体也是通过 IDL 生成的其内容在自动生成的 ttypes.go 文件中我们仍需在 thrift 入口将这个自动生成的结构体映射到 logic 入口所需的结构体上gRPC 也是类似。这部分代码还是需要的。仓库佐证pbgo 与 grpc-gateway 的多协议实践这种协议层与业务层解耦的思路在仓库的 RPC 章节中有完整落地。例如 ch4-rpc/ch4-07-pbgo.md 介绍的pbgo 迷你框架通过 Protobuf 扩展语法为 service 方法标注 REST 路由元信息service HelloService { rpc Hello (String) returns (String) { option (pbgo.rest_api) { get: /hello/:value }; } }插件读取扩展后生成HelloServiceHandler路由处理器把 HTTP 请求绑定到协议无关的参数结构体上再调用服务实现而 ch4-rpc/ch4-06-grpc-ext.md 介绍的grpc-gateway则通过option (google.api.http)为 gRPC 方法添加GET/POST映射生成RegisterRestServiceHandlerFromEndpoint将 REST 请求转发给后端 gRPC 服务。这正是本节所述Protocol 层独立、业务层协议无关思想的工程化体现。协议层的大量重复劳动用代码生成抽离聪明的读者已经看出来了协议细节处理这一层有大量重复劳动每一个接口在协议层的处理无非是把数据从协议特定的结构体例如http.Request、thrift 的被包装过了读出来绑定到协议无关的结构体上再映射到 Controller 入口的结构体上。这些代码长得都差不多而差不多的代码都遵循某种模式那么就可以对这些模式进行抽象用代码生成的方式把繁复的协议处理代码从工作内容中抽离出去。先来看看 HTTP 对应的结构体、thrift 对应的结构体和我们协议无关的结构体分别长什么样子// http 请求结构体 type CreateOrder struct { OrderID int64 json:order_id validate:required UserID int64 json:user_id validate:required ProductID int json:prod_id validate:required Addr string json:addr validate:required } // thrift 请求结构体 type FeatureSetParams struct { DriverID int64 thrift:driverID,1,required OrderID int64 thrift:OrderID,2,required UserID int64 thrift:UserID,3,required ProductID int thrift:ProductID,4,required Addr string thrift:Addr,5,required } // controller input struct type CreateOrderParams struct { OrderID int64 UserID int64 ProductID int Addr string }我们需要通过一个源结构体来生成需要的 HTTP 和 thrift 入口代码。观察上面三种结构体可以发现只要用一个结构体生成 thrift 的 IDL以及 HTTP 服务的IDL只要能包含 json 或 form 相关 tag 的结构体定义信息就可以了。这个初始结构体可以把 HTTP 的 tag 和 thrift 的 tag 揉在一起type FeatureSetParams struct { DriverID int64 thrift:driverID,1,required json:driver_id OrderID int64 thrift:OrderID,2,required json:order_id UserID int64 thrift:UserID,3,required json:user_id ProductID int thrift:ProductID,4,required json:prod_id Addr string thrift:Addr,5,required json:addr }然后通过代码生成把 thrift 的 IDL 和 HTTP 的请求结构体都生成出来图 5-16 通过 Go 代码定义结构体生成项目入口图源images/ch6-08-code-gen.png至于用什么手段来生成可以通过 Go 语言内置的 Parser 读取文本文件中的 Go 源代码根据 AST 来生成目标代码也可以简单地把这个源结构体和 Generator 的代码放在一起编译让结构体作为 Generator 的输入参数这样更简单一些。两种方式都可以。这种结构体标签驱动代码生成的思路与 pbgo 中结构体 tagthrift tag json tag驱动 REST/RPC 入口生成的实现方式完全同构——可见 ch4-rpc/ch4-07-pbgo.md 中getServiceMethodOption通过proto.HasExtension/proto.GetExtension读取扩展元信息后生成路由代码的实现细节。另一种思路从 thrift IDL 反向生成当然这种思路并不是唯一选择还可以通过解析 thrift 的 IDL生成一套 HTTP 接口的结构体。如果选择这么做整个流程就变成了图 5-17 也可以从 thrift 生成其它部分图源images/ch6-08-code-gen-2.png看起来比之前的图顺畅一点不过如果选择这么做需要自行对 thrift 的 IDL 进行解析相当于可能要手写一个 thrift IDL 的 Parser。虽然现在有 Antlr 或 peg 能帮忙简化 Parser 的书写工作但在解析这一步我们不希望引入太多工作量量力而行即可。既然工作流已经成型可以进一步琢磨怎么让整个流程对用户更加友好——比如在前面的生成环境引入 Web 页面让用户点点鼠标就能生成 SDK这些就靠读者自己去探索了。中间件与多协议尚未解决的现实问题虽然我们成功使项目入口支持了多种交互协议但还有一些问题没有解决本节所叙述的分层没有将中间件作为项目的分层考虑进去。如果考虑中间件请求的流程是什么样的图 5-18 加入中间件后的控制流图源images/ch6-08-control-flow-2.png之前在 ch5-web/ch5-03-middleware.md 中学习的中间件是和 HTTP 协议强相关的——本质是通过func(http.Handler) http.Handler对 handler 进行包装形成logger(timeout(ratelimit(helloHandler)))这样的函数链把耗时统计、限流、日志等非业务逻辑从业务代码中剥离。但遗憾的是在 thrift 中看起来没有和 HTTP 中对等的、解决非功能性逻辑代码重复问题的中间件所以图上写的是thrift stuff——这些 stuff 可能需要手写实现每次增加一个新的 thrift 接口就需要重写一遍这些非功能性代码。这也是很多企业项目面临的真实问题遗憾的是开源界并没有这样方便的多协议中间件解决方案。当然前面也说过很多时候我们给自己保留的 HTTP 接口只是用来做调试、并不会暴露给外人用这种情况下这些非功能性代码只要在 thrift 的代码中完成即可。分层实践要点总结结合本文的分层架构与仓库相关章节落地时可以遵循以下要点职责边界Controller 只做路由、参数校验与请求转发Logic/Service 承载全部业务规则Business Rules进入该层后参数保证合法DAO/Repository 只负责持久化与存储访问。协议无关性Controller 入口只接收ctx与协议无关的参数结构体代码中不应出现http.Request、http.ResponseWriter或 thrift/gRPC 字样。多协议支持将协议细节隔离在独立的 Protocol 层同一业务接口可同时暴露 thrift高性能与 http易调试两个入口。消除重复利用结构体 tagthrift tag json tag 揉合作为单一事实来源通过代码生成产出各协议入口参考 pbgo 与 grpc-gateway 的插件化实现。中间件边界HTTP 中间件已成熟见 ch5-web/ch5-03-middleware.md 与 chi 的compress/heartbeat/logger/profiler/realip/requestid/timeout/throttler等典型组件但 thrift 等多协议中间件仍缺乏对等方案需自行权衡。接口抽象时机业务早期不宜过早引入 interface待主流程稳定后再抽象见 ch5-web/ch5-08-interface-and-web.md并可用表驱动map 替代 switch降低入口处圈复杂度。这套 CLD Protocol 分层的架构思想配合代码生成与中间件治理是 Go 后端服务从单协议小服务演进为多协议、可扩展企业级服务的重要参考路径。【免费下载链接】advanced-go-programming-book:books: 《Go语言高级编程》开源图书涵盖CGO、Go汇编语言、RPC实现、Protobuf插件实现、Web框架实现、分布式系统等高阶主题(完稿)项目地址: https://gitcode.com/gh_mirrors/ad/advanced-go-programming-book创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考