本文主要是对大佬geektutu的7-days-golang下面的gee-web项目进行总结学习,方便大家理解。
1. 为什么需要Web框架
go提供的内置包net/http,实现了一些基础的Web功能,即监听端口,映射静态路由,解析HTTP报文。但这项相当于仅仅提供了一块一块积木,如果我们想要去真正实现一个Web服务需要考虑的更多,比如动态路由、鉴权、模版等功能,如果没有Web框架,我们就需要自己去将这些积木给搭建起来,这样每次新开一个项目都需要重新搭建一遍积木。有没有一种办法将这些积木先组合成模块,然后再给我们调用,我们仅仅需要组装模块即可,这样可以大大加快开发速度,这就有了Web框架的出现。Web框架将一些通用功能打包到框架里面,使用者就可以专注于业务逻辑即可。
2. net/http库
2.1 简单使用
go语言的net/http库使用起来非常简单,比如下面代码,定义了两个访问url,分别是/和/hello。然后仅需定义两个方法分别处理这两个url的访问即可。
func main() {
// 绑定 url 和对应的处理方法
http.HandleFunc("/", indexHandler)
http.HandleFunc("/hello", helloHandler)
// 在9999端口监听
log.Fatal(http.ListenAndServe(":9999", nil))
}
// 访问 / 的处理方法
func indexHandler(w http.ResponseWriter, req *http.Request) {
_, err := fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
// 访问 /hello 的处理方法
func helloHandler(w http.ResponseWriter, req *http.Request) {
for k, v := range req.Header {
_, err := fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
}
2.2 自定义handler
上面是net/http最简单的应用,只需要定义url以及对应处理的方法即可,将处理方法的调用全都交给框架处理。实际上还可以自定义一个http.Handler,将调用处理方法这些流程也放在自己手中控制。
如下,定义一个Engine结构体,其实现了ServeHTTP方法,在go语言中没有显式的接口实现,只有对应的结构体实现了某一接口定义的全部方法,那么就可以将该结构体成为那个接口的实现,同时在调用的过程中,go语言还可以对结构体进行隐式转换,将其转换成对应的接口类型。
在ServeHTTP方法里面,判断访问的URL,从而选择对应的方法,实现了跟1.1一样的功能。但是这种实现可以给予我们更多自由发挥的空间,比如我们可以自定义访问失败的返回。
type Engine struct{}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/":
indexHandler(w, req)
case "/hello":
helloHandler(w, req)
default:
_, err := fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
}
func main() {
engine := new(Engine)
log.Fatal(http.ListenAndServe(":9999", engine))
}
2.3 封装handler
上述实现虽然更灵活了一点,但是带了一个问题,如果我们每次要加新的URL访问都需要修改代码,并且所有的访问处理都在ServeHTTP接口中。所有需要对新加的http.handler进行抽象,让其成为一个routers,根据访问URL选择对应的处理方法。
首先封装处理函数HandlerFunc,URL对应的处理方法应该为该类型。
然后定义Engine结构体,Engine实现了ServeHTTP方法,在该方法中根据访问路径选择对应的处理方法。同时在Engine中定义一个map,存放URL和对应处理方法之间的映射。
最后定义一个方法,创建一个Engine实例。
type HandlerFunc func(http.ResponseWriter, *http.Request)
type Engine struct {
router map[string]HandlerFunc
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
key := req.Method + "-" + req.URL.Path
if handler, ok := engine.router[key]; ok {
handler(w, req)
} else {
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
}
}
func New() *Engine {
return &Engine{router: make(map[string]HandlerFunc)}
}
处理处理请求,Engine还需要具有新加route的能力,因此有了如下三个方法,GET是新建一个GET类型的route,POST是新建一个POST类型的route。
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
key := method + "-" + pattern
log.Printf("Route %4s - %s", method, pattern)
engine.router[key] = handler
}
除此之外,Engine还封装了net/http的启动方法。
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
3. Context功能
上述的代码还仅仅实现了net/http包所提供的基本功能,并没有提供一些更强大的功能,这个时候应该想一想如果需要一个更强大的功能,应该怎样去做,比如想要实现路由分组功能以及中间件功能。
首先路由(router)是一个独立的概念,可以独立出来,方便后续对路由功能进行增强,然后如果想要支持中间件功能,就需要在一个请求当中共享上下文信息,因此我们还需要有一个上下文(Context)模块。
首先定义Context的结构体,结构体中包含三类元素。首先是origin object(http.ResponseWriter、*http.Request),在之前我们已经知道这是一个route的处理函数所必须的输入参数;然后是跟请求有关的信息request info,Path和Method都是从http.ResponseWriter取出的信息;最后是跟响应有关的信息response info,StatusCode即响应码。
type Context struct {
// origin objects
Writer http.ResponseWriter
Req *http.Request
// request info
Path string
Method string
// response info
StatusCode int
}
然后为了简化接口,封装了一些http.Request方法以供使用,
func (c *Context) PostForm(key string) string {
return c.Req.FormValue(key)
}
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key)
}
封装一些http.ResponseWriter方法使用,为了方便对于JSON、HTML等返回类型的支持,这些返回类型都是非常常见的,因此封装起来,减少调用的代码量。
func (c *Context) Status(code int) {
c.StatusCode = code
c.Writer.WriteHeader(code)
}
func (c *Context) SetHeader(key string, value string) {
c.Writer.Header().Set(key, value)
}
func (c *Context) String(code int, format string, values ...interface{}) {
c.SetHeader("Content-Type", "text/plain")
c.Status(code)
c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
func (c *Context) JSON(code int, obj interface{}) {
c.SetHeader("Content-Type", "application/json")
c.Status(code)
encoder := json.NewEncoder(c.Writer)
if err := encoder.Encode(obj); err != nil {
http.Error(c.Writer, err.Error(), 500)
}
}
func (c *Context) Data(code int, data []byte) {
c.Status(code)
c.Writer.Write(data)
}
func (c *Context) HTML(code int, html string) {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
c.Writer.Write([]byte(html))
}
4. 动态路由
所谓动态路由,一条路由规则可以匹配某一类型而非某一条固定的路由,例如/hello/:name,可以匹配任何以/hello/为前缀的URL。前面使用map结构存储路由表,只能匹配静态路由,无法匹配动态路由。因此我们要使用前缀(Trie)树实现一个匹配动态路由的功能。
首先前缀树路由每个节点由以下四个部分构成:
pattern存放完整的路由路径,只有在某一个匹配路由规则最后一个节点才有值part存放路由路径中的某一部分children当前节点的子节点集合isWild是否精准匹配,如果part中含有:或者*那么该值为true
type node struct {
pattern string
part string
children []*node
isWild bool
}
然后对于一个路由来说,最重要的两个功能就是插入新的路由,和查询已有路由了。 如下为插入新路由的方法,采用递归的方式插入
func (n *node) insert(pattern string, parts []string, height int) {
if len(parts) == height {
n.pattern = pattern
return
}
// 拿出一个height位置的part
part := parts[height]
// 是否有与这个part相等的child
child := n.matchChild(part)
if child == nil {
// 如果没有的话就创建一个新的。
child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
n.children = append(n.children, child)
}
// 递归插入
child.insert(pattern, parts, height+1)
}
如下为查询已有路由的功能,当有新的访问进来的时候,应首先调用该方法查询当前访问是否在路由表中。
func (n *node) search(parts []string, height int) *node {
// 查完parts或者遇到一个通配符,那么看一下这个node是不是有pattern,如果有的话,说明存在这样一条路径,如果没有说明没有这样一条路径
if len(parts) == height || strings.HasPrefix(n.part, "*") {
if n.pattern == "" {
return nil
}
return n
}
// 当前part
part := parts[height]
// 查询当前node与part相等的所有children节点
children := n.matchChildren(part)
// 遍历符合条件的子节点,递归查询
for _, child := range children {
result := child.search(parts, height+1)
if result != nil {
return result
}
}
return nil
}
5. 路由分组功能
除了动态路由功能,与路由相关的最重要的一个功能应该是路由分组功能了。想要实现路由分组功能,首先需要抽象出一个RouterGroup的类型出来,这个类型应该能满足如下几个功能:
- 正确的分组
- 存储
group的相关信息 - 满足多层分组的需要(可以在
group里面再新建group) - 能够对某一类
group加中间件进行处理
因此要实现上面四个功能,对engine重新设计,首先RouterGroup代表分组类型,包含四部分信息,prefix当前group的前缀;middlewares当前分组需要执行的中间件处理函数;parent当前group的父group;engine所有的group共享一个engine对象,为了便于操作,可以直接在group中获取engine中的信息。
然后将group相关的信息也加入到engine中,这里需要注意的时候,一个engine就相当于一个没有前缀的分组,所有在engine中也支持group相关的所有方法,同时有一个groups存放所有的group信息。
type (
RouterGroup struct {
prefix string
middlewares []HandlerFunc // support middleware
parent *RouterGroup // support nesting
engine *Engine // all groups share a Engine instance
}
Engine struct {
*RouterGroup
router *router
groups []*RouterGroup // store all groups
}
)
上面也说了Engine相当于一个没有前缀的group,那么就可以将之前所有绑定在engine上,与路由相关的方法都可以绑定到RouterGroup上,这样就不需要在RouterGroup上再实现一遍路由相关的方法了。同时增加一个新建分组的方法,具体代码如下:
func (group *RouterGroup) Group(prefix string) *RouterGroup {
engine := group.engine
newGroup := &RouterGroup{
prefix: group.prefix + prefix,
parent: group,
engine: engine,
}
engine.groups = append(engine.groups, newGroup)
return newGroup
}
6. 中间件功能
6.1 中间件功能实现
在上面实现分组功能的时候,预留了添加中间件的位置。为什么需要中间件功能?Web框架本身不可能理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架当中。然后对于中间件来说,需要考虑2个比较关键的点:
- 插入位置,使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间间的逻辑就会变的复杂,如果插入点离用户太近,那用户完全可以自己调用,不需要框架的参与。
- 中间件的输入是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限。
因此在Web框架中,中间件的输入应该是
Context对象,这样可以基本满足所有的功能调用需要,插入点是框架接收到请求初始化Context对象后,允许用户使用自定义的中间件做一些额外处理,如记录日志或者对Context进行加工。
在实现方面,中间件应该与Group对象绑定,因为需要中间件的时候,肯定是要对一类路由进行处理。如果仅仅单个路由需要,那完全可以将逻辑放入到对应路由的处理函数里面。如下方法实现将中间件添加到group中去。
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middlewares = append(group.middlewares, middlewares...)
}
在具体流程中,也需要做一定调整,在之前没有添加中间件的处理流程里,当一个新的请求进来之后,直接就调用对应的处理函数即可。而加了中间件之后,整个处理流程就变成了,当一个新的请求进来之后,首选需要查出本次请求需要调用哪些中间件之后,然后保持下来放到context中,之后再查出本次请求对应的处理函数,然后再依次开始请求。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var middlewares []HandlerFunc
// 查出所有需要调用的中间件
for _, group := range engine.groups {
if strings.HasPrefix(req.URL.Path, group.prefix) {
middlewares = append(middlewares, group.middlewares...)
}
}
c := newContext(w, req)
c.handlers = middlewares
engine.router.handle(c)
}
因此我们需要在context对象中间件所需要的一些信息。
type Context struct {
// origin objects
Writer http.ResponseWriter
Req *http.Request
// request info
Path string
Method string
Params map[string]string
// response info
StatusCode int
// middleware
// 所有需要实现的handler方法
handlers []HandlerFunc
// 当前执行位置
index int
}
除此之外,还需要实现一个next方法,因为有一类中间件需要处理流程开始之前执行,在处理流程结束之后才结束,比如实现一个记录处理时间的中间件。
func (c *Context) Next() {
c.index++
s := len(c.handlers)
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
如下实现一个简单的记录处理时间的中间件函数
func recordTime() gee.HandlerFunc {
return func(c *gee.Context) {
// Start timer
start := time.Now()
// 调用后续处理函数
c.Next()
// Calculate resolution time
end := time.Now()
log.Printf("[%d] %s start in %s end in %s", c.StatusCode, c.Req.RequestURI, start, end)
}
}
6.2 错误恢复(Panic Recover)
上面我们已经实现了中间件功能,现在我们可以利用中间件功能给框架加上错误恢复功能,handle那些出错的情况,保证服务不会因为错误而停止。
Recovery函数的处理逻辑也非常简单,使用defer关键字定义一段在所有处理函数处理完成之后的逻辑。使用内置的recover函数判断是否错误,如果出错的话就调用
context上的fail方法。
func Recovery() HandlerFunc {
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
message := fmt.Sprintf("%s", err)
log.Printf("%s\n\n", trace(message))
c.Fail(http.StatusInternalServerError, "Internal Server Error")
}
}()
c.Next()
}
}
将该中间件挂在Engine所在的group上,这样就可以让所有的路由都实现错误恢复的功能了。