实际上只要你的handler函数签名是:
func (ResponseWriter, *Request)
那么这个 handler 和 http.HandlerFunc() 就有了一致的函数签名,可以将
该 handler() 函数进行类型转换,转为 http.HandlerFunc 。
而 http.HandlerFunc 实现了 http.Handler 这个接口。在 http 库需要调用你
的handler函数来处理http请求时,会调用 HandlerFunc() 的 ServeHTTP() 函
数,可见一个请求的基本调用链是这样的:
h = getHandler() => h.ServeHTTP(w, r) => h(w, r)
上面提到的把自定义 handler 转换为 http.HandlerFunc() 这个过程是必须
的,因为我们的 handler 没有直接实现 ServeHTTP 这个接口。上面的代码中我
们看到的HandleFunc(注意HandlerFunc和HandleFunc的区别)
r = NewRouter()
r.Use(logger)
r.Use(timeout)
r.Use(ratelimit)
r.Add("/", helloHandler)
通过多步设置,我们拥有了和上一节差不多的执行函数链。胜在直观易懂,如果我
们要增加或者删除中间件,只要简单地增加删除对应的 Use() 调用就可以了。非
常方便。
源Go语言框架chi为例:
5.3 中间件
451compress.go
=> 对http的响应体进行压缩处理
heartbeat.go
=> 设置一个特殊的路由,例如/ping,/healthcheck,用来给负载均衡一类的前
置服务进行探活
logger.go
=> 打印请求处理处理日志,例如请求处理时间,请求路由
profiler.go
=> 挂载pprof需要的路由,如`/pprof`、`/pprof/trace`到系统中
realip.go
=> 从请求头中读取X-Forwarded-For和X-Real-IP,将http.Request中的Rem
oteAddr修改为得到的RealIP
requestid.go
=> 为本次请求生成单独的requestid,可一路透传,用来生成分布式调用链路,也
可用于在日志中串连单次请求的所有逻辑
timeout.go
=> 用context.Timeout设置超时时间,并将其通过http.Request一路透传下去
throttler.go
=> 通过定长大小的channel存储token,并通过这些token对接口进行限流
validator:
前文中提到的校验场
景我们都可以通过validator完成工作。还以前文中的结构体为例。为了美观起见,
我们先把json tag省略掉。
这里我们引入一个新的validator库:
5.4 请求校验
456https://github.com/go-playground/validator
import "gopkg.in/go-playground/validator.v9" type RegisterReq struct { // 字符串的 gt=0 表示长度必须 > 0,gt = greater than Username string `validate:"gt=0"` // 同上 PasswordNew string `validate:"gt=0"` // eqfield 跨字段相等校验 PasswordRepeat string `validate:"eqfield=PasswordNew"` // 合法 email 格式校验 Email string `validate:"email"` } validate := validator.New() func validate(req RegisterReq) error { err := validate.Struct(req) if err != nil { doSomething() return err } . .. }
这样就不需要在每个请求进入业务逻辑之前都写重复的 validate() 函数了。本
例中只列出了这个校验器非常简单的几个功能
go sql ORM or SQL Builder
相比ORM来说,SQL Builder在SQL和项目可维护性之间取得了比较好的平衡。首
先sql builer不像ORM那样屏蔽了过多的细节,其次从开发的角度来讲,SQL
Builder简单进行封装后也可以非常高效地完成开发,举个例子:
where := map[string]interface{} { "order_id > ?" : 0, "customer_id != ?" : 0, } limit := []int{0,100} orderBy := []string{"id asc", "create_time desc"} orders := orderModel.GetList(where, limit, orderBy)
写SQL Builder的相关代码,或者读懂都不费劲。把这些代码脑内转换为sql也不会
太费劲。所以通过代码就可以对这个查询是否命中数据库索引,是否走了覆盖索
引,是否能够用上联合索引进行分析了。
说白了SQL Builder是sql在代码里的一种特殊方言,如果你们没有DBA但研发有自
己分析和优化sql的能力,或者你们公司的DBA对于学习这样一些sql的方言没有异
议。那么使用SQL Builder是一个比较好的选择,不会导致什么问题。
另外在一些本来也不需要DBA介入的场景内,使用SQL Builder也是可以的,例如
你要做一套运维系统,且将MySQL当作了系统中的一个组件,系统的QPS不高,
查询不复杂等等。
MVC
按照MVC的创始人的想法,我们如果把和数据打交道的代码还有业务流程全部塞进MVC里的M
层的话,这个M层又会显得有些过于臃肿。对于复杂的项目,一个C和一个M层显然
是不够用的,现在比较流行的纯后端API模块一般采用下述划分方法:
1. Controller,与上述类似,服务入口,负责处理路由,参数校验,请求转发。
2. Logic/Service,逻辑(服务) 层,一般是业务逻辑的入口,可以认为从这里开
始,所有的请求参数一定是合法的。业务逻辑和业务流程也都在这一层中。常
见的设计中会将该层称为 Business Rules。
3. DAO/Repository,这一层主要负责和数据、存储打交道。将下层存储以更简单
的函数、接口形式暴露给 Logic 层来使用。负责数据的持久化工作。
每一层都会做好自己的工作,然后用请求当前的上下文构造下一层工作所需要的结
构体或其它类型参数,然后调用下一层的函数。在工作完成之后,再把处理结果一
层层地传出到入口,
划分为CLD三层之后,在C层之前我们可能还需要同时支持多种协议。本章前面讲
到的thrift、gRPC和http并不是一定只选择其中一种,有时我们需要支持其中的两
种,比如同一个接口,我们既需要效率较高的thrift,也需要方便debug的http入口。
即除了CLD之外,还需要一个单独的protocol层,负责处理各种交互协议的细节。