仿照Go web框架gin手写自己的web框架 【中】

首先最终目的是模仿gin框架核心的几个功能就够了。

声明: 三部曲文章主要参考: https://geektutu.com/post/gee.html

所以最终的框架核心文件如下:

- gee/
      - context.go       // 上下文
      - gee.go            // gee核心函数
      - recovery.go     // 处理异常恢复中间件 并且trace错误
      - router.go        // 路由处理
      - trie.go            // 前缀树路由 (这一点实现比较复杂,可以参考 https://geektutu.com/post/gee-day3.html)

基础封装

实现一个最基础的一版 只包含一个gee.go文件的框架,通过上一章提到的实现 Handler接口的方式简单封装路由。

对于代码 github

- gee/
      - gee.go            // gee核心函数
  main.go                 // 用户引用文件

首先gee.go文件如下

package gee

import (
        "fmt"
        "net/http"
)

// HandlerFunc defines the request handler used by gee
type HandlerFunc func(http.ResponseWriter, *http.Request)

// Engine implement the interface of ServeHTTP
type Engine struct {
        // 存储请求方法-匹配参数 和 处理请求的函数
        router map[string]HandlerFunc
}

// New is the constructor of gee.Engine
func New() *Engine {
        return &Engine{router: make(map[string]HandlerFunc)}
}

// 把请求路径 和 处理函数放入 router 这个map中
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
        key := method + "-" + pattern
        engine.router[key] = handler
}

// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
        engine.addRoute("GET", pattern, handler)
}

// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
        engine.addRoute("POST", pattern, handler)
}

// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
        return http.ListenAndServe(addr, engine)
}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
        key := req.Method + "-" + req.URL.Path
        // 通过方法和请求路径 匹配到 handler请求处理函数
        if handler, ok := engine.router[key]; ok {
                handler(w, req)
        } else {
                w.WriteHeader(http.StatusNotFound)
                // 没有匹配到则表示 路径和请求方法不存在
                _, _ = fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
        }
}

用户使用gee框架 main.py文件

package main

import (
        "fmt"
        "net/http"
        "gee"
)

func main() {

        r := gee.New()

        r.GET("/", func(w http.ResponseWriter, req *http.Request) {
                _, _ = fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
        })

        r.GET("/hello", func(w http.ResponseWriter, req *http.Request) {
                for k, v := range req.Header {
                        _, _ = fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
                }
        })

        _ = r.Run("127.0.0.1:7052")
}

这种方式了,原理就是把路由和请求处理函数,存储到 Engine属性router 这个map中,然后每次请求进来,通过请求方法 - 请求路径的形式,来从map中匹配到请求响应函数,然后处理请求在返回。

封装上下文,路由 和 返回值

这一个是基于上一步,把响应的返回值还有响应状态,给封装了,方便返回不同的数据类型,比如JSON,字符串,或者HTML渲染。

此次章节代码如下

对应github代码

- gee/
      - context.go       // 上下文
      - gee.go            // gee核心函数
      - router.go        // 路由处理
  main.go                // 用户使用框架文件夹

router.go 单独抽取出路由文件

package gee

import "net/http"

type Route struct {
        // 存储 请求方式-路由 : 请求处理函数
        handlers map[string]HandlerFunc
}

// 初始化路由
func NewRoute() *Route {
        return &Route{
                handlers: make(map[string]HandlerFunc),
        }
}

// 路由内部添加添加方法
func (r *Route) addRoute(method string, pattern string, handler HandlerFunc) {
        // 拼接请求方式 和 路由
        key := method + "-" + pattern
        // 存储到路由处理的映射中 和 请求处理函数 一一对应
        r.handlers[key] = handler
}

// 找到并执行处理请求函数
func (r *Route) handle(c *Context) {
        key := c.Method + "-" + c.Path
        if handler, ok := r.handlers[key]; ok {
                handler(c)
        } else {
                // 没有找到直接 404
                c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
        }
}

gee.go文件,把路由模块单独抽出去了,只用在封装一层添加路由和匹配处理函数即可

package gee

import "net/http"

type Route struct {
        // 存储 请求方式-路由 : 请求处理函数
        handlers map[string]HandlerFunc
}

// 初始化路由
func NewRoute() *Route {
        return &Route{
                handlers: make(map[string]HandlerFunc),
        }
}

// 路由内部添加添加方法
func (r *Route) addRoute(method string, pattern string, handler HandlerFunc) {
        // 拼接请求方式 和 路由
        key := method + "-" + pattern
        // 存储到路由处理的映射中 和 请求处理函数 一一对应
        r.handlers[key] = handler
}

// 找到并执行处理请求函数
func (r *Route) handle(c *Context) {
        key := c.Method + "-" + c.Path
        if handler, ok := r.handlers[key]; ok {
                handler(c)
        } else {
                // 没有找到直接 404
                c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
        }
}

context.go请求上下封装,把常见的请求信息(获取请求参数信息),和响应信息(响应特定类型)封装到里面

package gee

import (
        "encoding/json"
        "fmt"
        "net/http"
)

// 用于返回JSON数据
type H map[string]interface{}

// 存储请求上下文信息
type Context struct {
        Req    *http.Request
        Writer http.ResponseWriter

        // 把一些基础信息单独抽出来
        Path       string
        Method     string
        StatusCode int
}

// 构建上下文实例
func NewContext(w http.ResponseWriter, r *http.Request) *Context {
        return &Context{
                Req:    r,
                Writer: w,
                Path:   r.URL.Path,
                Method: r.Method,
        }
}

// 获取url的查询参数
func (c *Context) Query(name string) string {
        return c.Req.URL.Query().Get(name)
}

// 获取表单参数
func (c *Context) PostForm(key string) string {
        return c.Req.FormValue(key)
}

// 设置状态码
func (c *Context) Status(code int) {
        c.StatusCode = code
        c.Writer.WriteHeader(code)
}

// 设置header
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;charset=utf-8")
        c.Status(code)
        _, _ = c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}

// 返回json数据
func (c *Context) JSON(code int, obj interface{}) {
        c.SetHeader("Content-Type", "application/json;charset=utf-8")
        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)
}

// 返回html数据
func (c *Context) HTML(code int, html string) {
        c.SetHeader("Content-Type", "text/html;charset=utf-8")
        c.Status(code)
        _, _ = c.Writer.Write([]byte(html))
}

用户main.go文件调用

package main

import (
        "practice/03_case_demo/09_custom_web_framework/04_gee_context/gee"
)

func main() {
        r := gee.New()

        r.GET("/", func(c *gee.Context) {
                c.String(200, "测试首页 你输入的name为%s 路径为 %s", c.Query("name"), c.Path)
        })

        r.GET("/json", func(c *gee.Context) {
                c.JSON(200, gee.H{
                        "json": "Value",
                })
        })

        _ = r.Run("127.0.0.1:7053")
}

总结

至此,一个最最基础的微框架 就完成了,但是还是缺少最核心的一些功能,比如路由分组,中间件处理,错误恢复机制。