15158846557 在线咨询 在线咨询
15158846557 在线咨询
所在位置: 首页 > 营销资讯 > 网站运营 > 自己动手写WEB框架-GO语言版

自己动手写WEB框架-GO语言版

时间:2023-07-24 04:39:01 | 来源:网站运营

时间:2023-07-24 04:39:01 来源:网站运营

自己动手写WEB框架-GO语言版:

Part0 写在前面

极客兔兔在学习大佬的教程的时候,由于基础不咋地,权当是自己的学习笔记,一点点的琢磨大佬的思路。

我的环境

go1.17.5 windows/amd64

goland 2021.3 且开启了go mod 功能

Part01

go原生方式实现一个web应用

import ( "fmt" "net/http")func main() { //注册路由 http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "hello go web") }) //启动 http.ListenAndServe(":8088", nil)}运行后通过访问 http://localhost:8088/ ,页面上展示 hello go web

在启动项目时,我们需要传递两个参数,很明显第一个是项目运行的端口号,而第二个参数,是项目运行时所依赖的实例,当传入的为nil时,将使用标准库中的实例,为了实现对请求的统一管理,我们需要自定义一个实例去实现Handler接口。

// 启动方法func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe()}//我们需要手动实现这个接口type Handler interface { ServeHTTP(ResponseWriter, *Request)}通过以下代码,我们使用自己定义的实例来管理所有请求,此时我们可以浏览器输入"http://localhost:8088/"或者是在其后面拼接任意请求,页面上都会将我们请求路径打印出来。

import ( "fmt" "net/http")type Entity struct {}// http.ResponseWriter 用于响应请求// *http.Request 包含了请求的详细信息func (e *Entity) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "你访问的路径是: %s/n", r.URL.Path)}func main() { e := &Entity{} http.ListenAndServe(":8088", e)}到现在为止,我们就实现了对请求的统一管理。

在我们拦截所有请求后,我们需要对请求实现分组控制,日志,异常处理等等。

// http.HandleFunc()方法的源码func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler)}首先我们需要检查用户访问的路由是否存在,因此我们需要使用一个map来存储路由和他的映射。首先我么为func(w http.ResponseWriter, r *http.Request)取一个别名,在接下来的代码中,我们以别名来表示此函数。

// 取别名type MyHandler func(w http.ResponseWriter, r *http.Request)type Entity struct { router map[string]*MyHandler}// 对外暴露初始化的操作func New() *Entity { return &Entity{router: make(map[string]MyHandler)}}接下来我们需要将方法添加到路由中。

// method:指请求方式GET或POST// pattern :指请求路由func (e *Entity) addRoute(method, pattern string, handler MyHandler) { key := method + "-" + pattern e.router[key] = handler}为了更加方便使用,我们需要再把addRoute方法封装为GET()和POST方法。这样我们在使用时只需要传入两个参数且不容易出错。

func (e *Entity) GET(pattern string, handler MyHandler) { e.addRoute("GET", pattern, handler)}func (e *Entity) POST(pattern string, handler MyHandler) { e.addRoute("POST", pattern, handler)}在前文中我们说到,在项目启动时我们需要传入实现了ServeHTTP()的实例,现在我们来对这个方法进行完善,最起码我们需要对用户访问的路由进行一个验证,最起码只能访问已注册了的路由。

func (e *Entity) ServeHTTP(w http.ResponseWriter, r *http.Request) { key := r.Method + "-" + r.URL.Path if handler, ok := e.router[key]; ok { handler(w, r) } else { fmt.Fprintf(w, "404 Not Found") }}此时我们再加上web项目启动方法,一个web的框架基本上就具备雏形了。

func (e *Entity) Run(port string) (err error) { return http.ListenAndServe(port, e)}在main()函数中进行测试

import ( "cin" "fmt" "net/http")func main() { c := cin.New() c.GET("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "hello world") }) c.Run(":8081")}截止到当前的代码存入网盘中,需要的可以下载。

百度网盘提取码:ccin

Part02 实现上下文

简单的来说,web服务就是收到请求(request) 、做出响应(response),而在一个完整的响应中,需要我们自己设置响应的消息头(包括状态码、消息类型)和消息体,这个时候有大量的重复性的工作,我们可以将此处设置响应头的工作抽取出来,以减轻开发时的工作量。

首先我们定义一个结构体(新建一个文件context.go),用来管理请求中的上下文,并编写初始化方法。

type Context struct { Writer http.ResponseWriter Req *http.Request Path string Method string StatusCode int}func newContext(w http.ResponseWriter, req *http.Request) *Context { return &Context{ Writer: w, Req: req, Path: req.URL.Path, Method: req.Method, }}编写设置状态码、设置自定义头部的方法。

func (c *Context) Status(code int) { c.StatusCode = code c.Writer.WriteHeader(code)}func (c *Context) setHeader(k, v string) { c.Writer.Header().Set(k, v)}提供公共方法,分别用于返回string、json以及html类型的数据。

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) HTML(code int, html string) { c.SetHeader("Content-Type", "text/html") c.Status(code) c.Writer.Write([]byte(html))}func (c *Context) DATA(code int, data []byte) { c.Status(code) c.Writer.Write(data)}添加访问Query和PostForm方法

func (c *Context) Query(s string) string { return c.Req.URL.Query().Get(s)}func (c *Context) PostForm(key string) string { return c.Req.FormValue(key)}为了使代码结构更加清晰,将路由相关的方法提取出来放在一个新文件中,同时修改与之相关的方法。修改后的文件及目录结构见链接,提取码:ccin

part03 实现前缀树路由

使用map存储路由表,在进行索引时效率很高,但是有一个很明显的弊端,那就是只能存储静态路由。因此我们需要改造路由表的存储方式,使其能够存储动态路由(例如/hello/:name或者是*这样来表示全量的路由)。然后我们需要通过前缀树来作为路由表的数据结构,前缀树最大的特点就是一个节点的所有子节点具有相同的前缀。在HTTP请求中的路由,以/路由1/路由2/路由3....这样的形式来表示,天然的就适合使用前缀树路由。同时我们约定,在定义路由时,如使用/:name这样的形式,那么这个路由可以匹配/a 或 /b 等等。

type node struct { pattern string // 待匹配路由 part string // 路由中的一部分 children []*node // 子节点 isWild bool // 是否精确匹配,part 含有 : 或 * 时为true}我们定义上述结构体,同时定义以下两个方法分别用于插入和查找。

func (n *node) matchChild(part string) *node { for _, child := range n.children { if child.part == part || child.isWild { return child } } return nil}func (n *node) matchChildren(part string) []*node { nodes := make([]*node, 0) for _, child := range n.children { if child.part == part || child.isWild { nodes = append(nodes, child) } } return nodes}接下来,我们编写一个路由插入的方法和路由查询的方法。

当我们往路由表中插入一个路由时,需要传入全路由名以及其数组(去掉/)以及路由的深度。

//插入func (n *node) insert(pattern string, parts []string, height int) { // 如果入参的路由的深度(数组parts长度)与入参的深度相同,则直接将这个路由插入 if len(parts) == height { n.pattern = pattern return } //查看当前路由是否存在 part := parts[height] 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 { // if len(parts) == height || strings.HasPrefix(n.part, "*") { if n.pattern == "" { return nil } return n } part := parts[height] children := n.matchChildren(part) for _, child := range children { result := child.search(parts, height+1) if result != nil { return result } } return nil}接下来就可以修改router,使其应用前缀树维护路由表。修改路由的代码。我们需要维护两个变量,分别用于存储handler映射和前缀树节点。

type router struct { handlers map[string]HandlerFunc roots map[string]*node}func newRouter() *router { return &router{ handlers: make(map[string]HandlerFunc), roots: make(map[string]*node), }}在我们前缀树路由的相关方法中,使用到了一个数组变量,这是由全路由以 "/"分割,将分割后的组成一个数组。例如全路由为/a/b/c,其数组就由a、b、c组成。

func parsePattern(pattern string) []string { vs := strings.Split(pattern, "/") parts := make([]string, 0) for _, v := range vs { if v != "" { parts = append(parts, v) if v[0] == '*' { break } } } return parts}编写新增路由和获取路由的方法

func (r *router) addRoute(method, pattern string, handler HandlerFunc) { parts := parsePattern(pattern) key := method + "-" + pattern _, ok := r.roots[method] if !ok { r.roots[method] = &node{} } r.roots[method].insert(pattern, parts, 0) r.handlers[key] = handler}func (r *router) getRoute(method, path string) (*node, map[string]string) { searchParts := parsePattern(path) params := make(map[string]string) root, ok := r.roots[method] if !ok { return nil, nil } n := root.search(searchParts, 0) if n != nil { parts := parsePattern(n.pattern) for i, part := range parts { if part[0] == ':' { params[part[1:]] = searchParts[i] } if part[0] == '*' && len(part) > 1 { params[part[1:]] = strings.Join(searchParts[i:], "/") break } } return n, params } return nil, nil}由于router的结构发生了变化,因此重写handle方法,在此之前也要修改context的结构和相关方法

type Context struct { Writer http.ResponseWriter Req *http.Request Path string Method string StatusCode int Params map[string]string}func (c *Context) Param(key string) string { value, _ := c.Params[key] return value}以下是修改后的handle方法

func (r *router) handle(c *Context) { n, params := r.getRoute(c.Method, c.Path) if n != nil { c.Params = params key := c.Method + "-" + n.pattern r.handlers[key](c) } else { c.String(http.StatusNotFound, "404 NOT FOUND: %s/n", c.Path) }}接下来进行相关测试提取码:ccin

part04路由分组

我们约定,一组路由具有相同的前缀(/xxx),当我们为路由分组以后,可以以组为单位为不同的路由设置鉴权、日志等功能,也方便我们管理。定义group结构体,用来表示路由的分组。同时修改Engine,将其作为一个顶级分组。

type RouterGroup struct { prefix string middlewares []HandlerFunc parent *RouterGroup engine *Engine }type Engine struct { router *router *RouterGroup groups []*RouterGroup}修改Engine的初始化方法

func New() *Engine { engine := &Engine{router: newRouter()} engine.RouterGroup = &RouterGroup{} engine.groups = []*RouterGroup{engine.RouterGroup} return engine}新增创建分组的方法

func (group *RouterGroup) Group(pre string) *RouterGroup { engine := group.engine newGroup := &RouterGroup{ prefix: group.prefix + pre, parent: group, engine: engine, } engine.groups = append(engine.groups, newGroup) return newGroup}修改注册路由的GET和POST方法以及addroute方法

func (group *RouterGroup) addRoute(method string, pattern string, handler HandlerFunc) { newPattern := group.prefix + pattern log.Printf("Route %4s - %s", method, newPattern) group.engine.router.addRoute(method, newPattern, handler)}func (group *RouterGroup) GET(pattern string, handler HandlerFunc) { group.addRoute("GET", pattern, handler)}func (group *RouterGroup) POST(pattern string, handler HandlerFunc) { group.addRoute("POST", pattern, handler)}提取码:ccin

part05中间件

我们需要在框架中加入一些非业务类的功能,比如打印日志。

func Logger() HandlerFunc { return func(c *Context) { // Start timer t := time.Now() // Process request c.Next() // Calculate resolution time log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t)) }}首先修改context的结构和初始化方法,并新增next方法。

type Context struct { Writer http.ResponseWriter Req *http.Request Path string Method string StatusCode int Params map[string]string handlers []HandlerFunc index int}func newContext(w http.ResponseWriter, req *http.Request) *Context { return &Context{ Writer: w, Req: req, Path: req.URL.Path, Method: req.Method, index: -1, }}func (c *Context) Next() { c.index++ s := len(c.handlers) for ; c.index < s; c.index++ { c.handlers[c.index](c) }}为GroupRouter新增方法

func (group *RouterGroup) Use(middlewares ...HandlerFunc) { group.middlewares = append(group.middlewares, middlewares...)}修改ServeHTTP方法

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)}修改handle方法

func (r *router) handle(c *Context) { n, params := r.getRoute(c.Method, c.Path) if n != nil { key := c.Method + "-" + n.pattern c.Params = params c.handlers = append(c.handlers, r.handlers[key]) } else { c.handlers = append(c.handlers, func(c *Context) { c.String(http.StatusNotFound, "404 NOT FOUND: %s/n", c.Path) }) } c.Next()}新增Fail方法

func (c *Context) Fail(code int, err string) { c.index = len(c.handlers) c.JSON(code, H{"message": err})}提取码:ccin

关键词:语言,动手

74
73
25
news

版权所有© 亿企邦 1997-2025 保留一切法律许可权利。

为了最佳展示效果,本站不支持IE9及以下版本的浏览器,建议您使用谷歌Chrome浏览器。 点击下载Chrome浏览器
关闭