前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >days-golang

days-golang

原创
作者头像
f1sh
修改2024-07-18 15:42:57
1240
修改2024-07-18 15:42:57

go语言之认识web框架

这几天学习了一下geektutu大佬的7days-golang项目(只简单学习了一下web框架-Gee部分),在这里进行一些简单的总结。

先在这里放一张大致的流程图

image-20240711165749048
image-20240711165749048

day1-handler

base1

第一部分简单介绍了一下net/http库和http.Handler接口,同时搭建Gee框架的雏形。

首先go里面是已经内置了net/http库的,里面封装了http网络编程的基础的接口。我们实现Gee这个web框架就是基于net/http实现的,

代码语言:javascript
复制
 func main() {
     http.HandleFunc("/", indexHandler)
     http.HandleFunc("/hello", helloHandler)
     log.Fatal(http.ListenAndServe(":9999", nil))
     //用来启动web服务 :9999是端口  nil是一个预先声明的标识符,是一个变量,表示指针、通道、函数、接口、映射或切片类型的零值,并不是GO 的关键字。在这里表示使用标准库中的实例处理。这个参数是我们基于net/http标准库实现web框架的入口。
 }
 ​
 // handler echoes r.URL.Path
 func indexHandler(w http.ResponseWriter, req *http.Request) {
     fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
 }
 //这个函数,会将请求的url路径写入响应中,w http.ResponseWriter这个参数用来构建http响应, req *http.Request这个参数包含http请求的详细信息。客户端发起请求时,服务器在响应中返回请求的url路径
 ​
 // handler echoes r.URL.Header
 func helloHandler(w http.ResponseWriter, req *http.Request) {
     for k, v := range req.Header {
         fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
     }
 }
 //for循环遍历req.Header所有头部信息,对于每个头部键值对,使用fmt.Fprintf将其格式化并写入响应。
 ​

访问/,响应是URL.Path = /,而/hello的响应则是请求头(header)中的键值对信息。

base2

先举个栗子

代码语言:javascript
复制
 package main
 ​
 import (
     "fmt"
     "net/http"
 )
 ​
 // 定义一个类型 MyHandler 实现 Handler 接口
 type MyHandler struct{}
 ​
 func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     fmt.Fprintf(w, "Hello, World!")
 }
 ​
 func main() {
     // 实例化 MyHandler
     handler := &MyHandler{}
 ​
     // 启动 HTTP 服务器
     err := http.ListenAndServe(":8080", handler)
     if err != nil {
         fmt.Println("Error starting server:", err)
     }
 }
 ​

这个代码展示了如何定义一个实现了 http.Handler 接口的处理器,并使用 http.ListenAndServe 启动一个 HTTP 服务器。在 ServeHTTP 方法中,处理器简单地响应 "Hello, World!"。 ps:ServeHTTP 方法是 http.Handler 接口的唯一方法只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了

代码语言:javascript
复制
 type Engine struct{}
 //定义了一个空的结构体,用来实现ServeHttp,
 ​
 func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
     switch req.URL.Path {
     case "/":
         fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
     case "/hello":
         for k, v := range req.Header {
             fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
         }
     default:
         fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
     }
 }
 ​
 func main() {
     engine := new(Engine)
     log.Fatal(http.ListenAndServe(":9999", engine))
 }

和上面的栗子相近。

在实现Engine之前,我们调用 http.HandleFunc 实现了路由和Handler的映射,也就是只能针对具体的路由写处理逻辑(一个具体的路由对应一个规则)。比如/hello。但是在实现Engine之后,我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。

base3

base3/go.mod

代码语言:javascript
复制
 module example
 ​
 go 1.13
 ​
 require gee v0.0.0
 ​
 replace gee => ./gee
 ​

从 go 1.11 版本开始,引用相对路径的 package 需要使用上述方式。

base3/main.go

代码语言:javascript
复制
 package main
 ​
 // $ curl http://localhost:9999/
 // URL.Path = "/"
 // $ curl http://localhost:9999/hello
 // Header["Accept"] = ["*/*"]
 // Header["User-Agent"] = ["curl/7.54.0"]
 // curl http://localhost:9999/world
 // 404 NOT FOUND: /world
 ​
 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(":9999")
 }
 ​

base3/gee/gee.go

代码语言:javascript
复制
 package gee
 ​
 import (
     "fmt"
     "log"
     "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)}
 }
 ​
 func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
     key := method + "-" + pattern
     log.Printf("Route %4s - %s", 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, ok := engine.router[key]:在路由表中查找对应键的处理器函数。
 //engine.router 是一个 map[string]HandlerFunc,存储了所有的路由键和处理器函数。
 //handler:如果键存在,handler 将是对应的处理器函数。
 //ok:一个布尔值,表示键是否存在于路由表中。
     if handler, ok := engine.router[key]; ok {
         handler(w, req)
     } else {
         fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
     }
 }
 ​

首先定义了类型HandlerFunc,这是提供给框架用户的,用来定义路由映射的处理方法。我们在Engine中,添加了一张路由映射表router,key 由请求方法和静态路由地址构成,例如GET-/GET-/helloPOST-/hello,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法(Handler),value 是用户映射的处理方法。 当用户调用(*Engine).GET()方法时,会将路由和处理方法注册到映射表 router 中,(*Engine).Run()方法,是 ListenAndServe 的包装。 Engine实现的 ServeHTTP 方法的作用就是,解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回 404 NOT FOUND

至此,整个Gee框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,包装了启动服务的函数。

day2-context

对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。

针对使用场景,封装*http.Requesthttp.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name,参数:name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。

gee/context.go

代码语言:javascript
复制
 type H map[string]interface{}
 ​
 type Context struct {
     // origin objects
     Writer http.ResponseWriter
     Req    *http.Request
     // request info
     Path   string
     Method string
     // response info
     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) PostForm(key string) string {
     return c.Req.FormValue(key)
 }
 ​
 func (c *Context) Query(key string) string {
     return c.Req.URL.Query().Get(key)
 }
 ​
 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))
 }
 ​
  • 代码最开头,给map[string]interface{}起了一个别名gee.H,构建JSON数据时,显得更简洁。
  • Context目前只包含了http.ResponseWriter*http.Request,另外提供了对 Method 和 Path 这两个常用属性的直接访问。
  • 提供了访问Query和PostForm参数的方法。
  • 提供了快速构造String/Data/JSON/HTML响应的方法。

这部分代码整体不难理解和day1代码难度基本一样,只是大致修改了一下参数。

gee/router.go

代码语言:javascript
复制
 type router struct {
     handlers map[string]HandlerFunc
 }
 ​
 func newRouter() *router {
     return &router{handlers: make(map[string]HandlerFunc)}
 }
 ​
 func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
     key := method + "-" + pattern
     r.handlers[key] = handler
 }
 ​
 func (r *router) handle(c *Context) {
     key := c.Method + "-" + c.Path
     if handler, ok := r.handlers[key]; ok {
         handler(c)
     } else {
         c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
     }
 }
 ​

代码基本没动,只是把与路由有关的方法和结构提取了出来,放到了新的文件中

gee/gee.go

代码语言:javascript
复制
package gee

import (
	"log"
	"net/http"
)

// HandlerFunc defines the request handler used by gee
type HandlerFunc func(*Context)

// Engine implement the interface of ServeHTTP
type Engine struct {
	router *router
}

// New is the constructor of gee.Engine
func New() *Engine {
	return &Engine{router: newRouter()}
}

func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
	log.Printf("Route %4s - %s", method, pattern)
	engine.router.addRoute(method, pattern, 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) {
	c := newContext(w, req)
	engine.router.handle(c)
    /*创建上下文对象:newContext(w, req) 创建并初始化一个 Context 对象,封装了 HTTP 请求和响应相关的信息。
调用路由处理方法:engine.router.handle(c) 将上下文对象传递给路由器的处理方法,根据请求信息找到对应的处理函数并执行。*/
}

相比第一天的代码,这个方法也有细微的调整,在调用 router.handle 之前,构造了一个 Context 对象。

day3

前面使用了map结构来存储了路由表,索引很高效,但是这个方式只能用来索引静态路由,但是无法支持动态路由

动态路由就是一条路由规则可以匹配某一类型而非某一条固定的路由

实现动态路由有很多方法:开源的gorouter支持在路由规则嵌入正则,另一个开源的httprouter就不支持正则。

实现动态路由最常用的数据结构是前缀树(trie树):每一个节点的所有子节点都拥有相同的前缀,这种结构很适用于路由匹配。举个栗子:

  • /:lang/doc
  • /:lang/tutorial
  • /:lang/intro
  • /about
  • /p/blog
  • /p/related

http请求的路径恰好是由/分隔的多段内容构成的,因此,每一段可以作为前缀树的一个节点。我们通过树结构查询,如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束。

接下来需要实现的动态路由具有以下两个功能。

  • 参数匹配:。例如 /p/:lang/doc,可以匹配 /p/c/doc/p/go/doc
  • 通配*。例如 /static/*filepath,可以匹配/static/fav.ico,也可以匹配/static/js/jQuery.js,这种模式常用于静态服务器,能够递归地匹配子路径。

具体实现

gee/grie.go

代码语言:javascript
复制
type node struct {
	pattern  string
	part     string
	children []*node
	isWild   bool
}

func (n *node) String() string {
	return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}", n.pattern, n.part, n.isWild)
}

func (n *node) insert(pattern string, parts []string, height int) {
	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
}
/*insert 方法通过递归方式,将路由模式的各个部分依次插入前缀树中,构建路由树。
search 方法通过递归方式,在前缀树中查找匹配的路由模式,支持静态路由和通配符路由的匹配。*/

func (n *node) travel(list *([]*node)) {
	if n.pattern != "" {
		*list = append(*list, n)
	}
	for _, child := range n.children {
		child.travel(list)
	}
}

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
}
//这两个方法整体逻辑差别不大,一个是返回单个子节点,另一个是返回所有匹配的子节点
//matchChild: 在路由树插入新节点时,找到合适的子节点进行插入。
//matchChildren: 在路由树查找节点时,找到所有可能的匹配路径。

gee/router.go

代码语言:javascript
复制
type router struct {
	roots    map[string]*node
	handlers map[string]HandlerFunc
}

func newRouter() *router {
	return &router{
		roots:    make(map[string]*node),
		handlers: make(map[string]HandlerFunc),
	}
}

// Only one * is allowed
func parsePattern(pattern string) []string {
	vs := strings.Split(pattern, "/")

	parts := make([]string, 0)
	for _, item := range vs {
		if item != "" {
			parts = append(parts, item)
			if item[0] == '*' {
				break
			}
		}
	}
	return parts
}

func (r *router) addRoute(method string, 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 string, 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 index, part := range parts {
			if part[0] == ':' {
				params[part[1:]] = searchParts[index]
			}
			if part[0] == '*' && len(part) > 1 {
				params[part[1:]] = strings.Join(searchParts[index:], "/")
				break
			}
		}
		return n, params
	}

	return nil, nil
}

func (r *router) getRoutes(method string) []*node {
	root, ok := r.roots[method]
	if !ok {
		return nil
	}
	nodes := make([]*node, 0)
	root.travel(&nodes)
	return nodes
}
/*
r.roots 是一个 map[string]*node,其中键是 HTTP 方法(如 GET、POST),值是对应的路由树的根节点。
root 是根据 method 从 r.roots 中获取的值,即对应的根节点。
ok 是一个布尔值,表示键 method 是否存在于 r.roots 中。
*/
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)
	}
}

这里使用 roots 来存储每种请求方式的Trie 树根节点。使用 handlers 存储每种请求方式的 HandlerFunc 。getRoute 函数中,还解析了:*两种匹配符的参数,返回一个 map 。例如/p/go/doc匹配到/p/:lang/doc,解析结果为:{lang: "go"}/static/css/geektutu.css匹配到/static/*filepath,解析结果为{filepath: "css/geektutu.css"}

day4-Group

所谓分组是指路由的分组,如果没有路由的分组,我们就需要对每一个路由进行控制,但是在具体的业务中,一部分路由总是需要进行相似的处理。举个栗子

  • /post开头的路由匿名可访问。
  • /admin开头的路由需要鉴权。
  • /api开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。

大部分情况的路由分组,是用相同的前缀进行区分的,所以这里实现的分组控制也是以前缀来区分,并且支持分组的嵌套。例如/post是一个分组,/post/a/post/b可以是该分组下的子分组。作用在/post分组上的中间件(middleware),也都会作用在子分组,子分组还可以应用自己特有的中间件。

一个 Group 对象需要具备哪些属性呢?首先是前缀(prefix),比如/,或者/api;要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁;当然了,按照我们一开始的分析,中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares)。还记得,我们之前调用函数(*Engine).addRoute()来映射所有的路由规则和 Handler 。如果Group对象需要直接映射路由规则的话,比如我们想在使用框架时,这时调用:

代码语言:javascript
复制
r := gee.New()
v1 := r.Group("/v1")
v1.GET("/", func(c *gee.Context) {
	c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})

因此Group对象,还需要有访问Router的能力,为了方便,我们可以在Group中,保存一个指针,指向Engine,整个框架的所有资源都是由Engine统一协调的,那么就可以通过Engine间接地访问各种接口了。

gee/gee.go

代码语言:javascript
复制
package gee

import (
	"log"
	"net/http"
)

// HandlerFunc defines the request handler used by gee
type HandlerFunc func(*Context)

// Engine implement the interface of ServeHTTP
type (
	RouterGroup struct {
		prefix      string
		middlewares []HandlerFunc // support middleware
		parent      *RouterGroup  // support nesting
		engine      *Engine       // all groups share a Engine instance
	}
    //Group 的定义

	Engine struct {
		*RouterGroup
		router *router
		groups []*RouterGroup // store all groups
	}
)
//进一步地抽象,将Engine作为最顶层的分组,也就是说Engine拥有RouterGroup所有的能力。

// New is the constructor of gee.Engine
func New() *Engine {
	engine := &Engine{router: newRouter()}
	engine.RouterGroup = &RouterGroup{engine: engine}
	engine.groups = []*RouterGroup{engine.RouterGroup}
	return engine
}

// Group is defined to create a new RouterGroup
// remember all groups share the same Engine instance
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
}

func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
	pattern := group.prefix + comp
	log.Printf("Route %4s - %s", method, pattern)
	group.engine.router.addRoute(method, pattern, handler)
}
//调用了group.engine.router.addRoute来实现了路由的映射。由于Engine从某种意义上继承了RouterGroup的所有属性和方法,因为 (*Engine).engine 是指向自己的。这样实现,我们既可以像原来一样添加路由,也可以通过分组添加路由。

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

// POST defines the method to add POST request
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
	group.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) {
	c := newContext(w, req)
	engine.router.handle(c)
}

day5-middlewares

中间件(middlewares),简单说,就是非业务的技术类组件。

有点像接口那种感觉

对中间件而言,需要考虑2个比较关键的点:

  • 插入点在哪?使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 中手工调用没有多大的优势了。
  • 中间件的输入是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限

geektutu大佬这里参考了gin

具体设计

gee/logger.go

代码语言:javascript
复制
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))
	}
}

Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是Context对象。插入点是框架接收到请求初始化Context对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对Context进行二次加工。另外通过调用(*Context).Next()函数,中间件可等待用户自己定义的 Handler处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作。举个例子,我们希望最终能够支持如下定义的中间件,c.Next()表示等待执行其他的中间件或用户的Handler: ps:支持多个中间件,依次进行调用

中间件是应用在RouterGroup上的,应用在最顶层的 Group,相当于作用于全局,所有的请求都会被中间件处理。只作用在某条路由规则的功能通用性太差,不适合定义为中间件。

当接收到请求后,匹配路由,该请求的所有信息都保存在Context中。中间件也不例外,接收到请求后,应查找所有应作用于该路由的中间件,保存在Context中,依次进行调用。为什么依次调用后,还需要在Context中保存呢?因为在设计中,中间件不仅作用在处理流程前,也可以作用在处理流程后,即在用户定义的 Handler 处理完毕后,还可以执行剩下的操作。

为此,我们给Context添加了2个参数,定义了Next方法:

gee/context.go

代码语言:javascript
复制
type H map[string]interface{}

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
	handlers []HandlerFunc
	index    int
}

func newContext(w http.ResponseWriter, req *http.Request) *Context {
	return &Context{
		Path:   req.URL.Path,
		Method: req.Method,
		Req:    req,
		Writer: w,
		index:  -1,
	}
}

func (c *Context) Next() {
	c.index++
	s := len(c.handlers)
	for ; c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}

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

func (c *Context) Param(key string) string {
	value, _ := c.Params[key]
	return value
}

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)
}

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))
}

index是记录当前执行到第几个中间件,当在中间件中调用Next方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在Next方法之后定义的部分。如果我们将用户在映射路由时定义的Handler添加到c.handlers列表中,结果会怎么样呢?想必你已经猜到了。

代码语言:javascript
复制
func A(c *Context) {
    part1
    c.Next()
    part2
}
func B(c *Context) {
    part3
    c.Next()
    part4
}

设我们应用了中间件 A 和 B,和路由映射的 Handler。c.handlers是这样的[A, B, Handler],c.index初始化为-1。调用c.Next(),最终的顺序是part1 -> part3 -> Handler -> part 4 -> part2

在gee.go中定义Use函数,将中间件应用到某个 Group 。

在router.go中将从路由匹配得到的 Handler 添加到 c.handlers列表中,执行c.Next()

代码语言:javascript
复制
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()
}

day6-template

现在越来越流行前后端分离的开发模式,即 Web 后端提供 RESTful 接口,返回结构化的数据(通常为 JSON 或者 XML)。前端使用 AJAX 技术请求到所需的数据,利用 JavaScript 进行渲染。

但前后分离的一大问题在于,页面是在客户端渲染的,比如浏览器,这对于爬虫并不友好。Google 爬虫已经能够爬取渲染后的网页,但是短期内爬取服务端直接渲染的 HTML 页面仍是主流。

要做到服务端渲染,第一步便是要支持 JS、CSS 等静态文件。之前设计动态路由的时候,支持通配符*匹配多级子路径。比如路由规则/assets/*filepath,可以匹配/assets/开头的所有的地址。例如/assets/js/geektutu.js,匹配后,参数filepath就赋值为js/geektutu.js

如果将所有的静态文件放在/usr/web目录下,那么filepath的值即是该目录下文件的相对地址。映射到真实的文件后,将文件返回,静态服务器就实现了。

找到文件后,如何返回这一步,net/http库已经实现了。因此,gee 框架要做的,仅仅是解析请求的地址,映射到服务器上文件的真实地址,交给http.FileServer处理就好了。

gee/gee.go

代码语言:javascript
复制
// create static handler
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
	absolutePath := path.Join(group.prefix, relativePath)
	fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
	return func(c *Context) {
		file := c.Param("filepath")
		// Check if file exists and/or if we have permission to access it
		if _, err := fs.Open(file); err != nil {
			c.Status(http.StatusNotFound)
			return
		}

		fileServer.ServeHTTP(c.Writer, c.Req)
	}
}

// serve static files
func (group *RouterGroup) Static(relativePath string, root string) {
	handler := group.createStaticHandler(relativePath, http.Dir(root))
	urlPattern := path.Join(relativePath, "/*filepath")
	// Register GET handlers
	group.GET(urlPattern, handler)
}

我们给RouterGroup添加了2个方法,Static这个方法是暴露给用户的。用户可以将磁盘上的某个文件夹root映射到路由relativePath。例如:

代码语言:javascript
复制
r := gee.New()
r.Static("/assets", "/usr/geektutu/blog/static")
// 或相对路径 r.Static("/assets", "./static")
r.Run(":9999")

用户访问localhost:9999/assets/js/geektutu.js,最终返回/usr/geektutu/blog/static/js/geektutu.js

Go语言内置了text/templatehtml/template2个模板标准库,其中html/template为 HTML 提供了较为完整的支持。包括普通变量渲染、列表渲染、对象渲染等。gee 框架的模板渲染直接使用了html/template提供的能力。

代码语言:javascript
复制
Engine struct {
	*RouterGroup
	router        *router
	groups        []*RouterGroup     // store all groups
	htmlTemplates *template.Template // for html render
	funcMap       template.FuncMap   // for html render
}

func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
	engine.funcMap = funcMap
}

func (engine *Engine) LoadHTMLGlob(pattern string) {
	engine.htmlTemplates = template.Must(template.New("").Funcs(engine.funcMap).ParseGlob(pattern))
}

首先为 Engine 示例添加了 *template.Templatetemplate.FuncMap对象,前者将所有的模板加载进内存,后者是所有的自定义模板渲染函数。

另外,给用户分别提供了设置自定义渲染函数funcMap和加载模板的方法。

接下来,对原来的 (*Context).HTML()方法做了些小修改,使之支持根据模板文件名选择模板进行渲染。

gee/context.go

代码语言:javascript
复制
type Context struct {
    // ...
	// engine pointer
	engine *Engine
}

func (c *Context) HTML(code int, name string, data interface{}) {
	c.SetHeader("Content-Type", "text/html")
	c.Status(code)
	if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil {
		c.Fail(500, err.Error())
	}
}

我们在 Context 中添加了成员变量 engine *Engine,这样就能够通过 Context 访问 Engine 中的 HTML 模板。实例化 Context 时,还需要给 c.engine 赋值。

gee/gee.go

代码语言:javascript
复制
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// ...
	c := newContext(w, req)
	c.handlers = middlewares
	c.engine = engine
	engine.router.handle(c)
}

最终的目录结构

代码语言:javascript
复制
---gee/
---static/
   |---css/
        |---geektutu.css
   |---file1.txt
---templates/
   |---arr.tmpl
   |---css.tmpl
   |---custom_func.tmpl
---main.go

day7-error

先简单了解一下什么是panic defer recover

在Go语言中,panicdeferrecover 是处理异常的一套机制。下面是它们的具体作用和使用方法:

panic

panic 用于在程序运行过程中发生严重错误时中止函数的正常执行。panic 可以携带一个错误信息,用于描述发生的错误。调用 panic 后,当前函数的执行将被立即终止,之后会逐层向上传递,直到程序退出或某个上层函数通过 recover 处理了这个 panic

代码语言:javascript
复制
func mightGoWrong() {
    panic("something went wrong")
}

defer

defer 语句用于延迟函数的执行,直到包含 defer 的函数执行完毕。这通常用于确保一些必要的清理工作能够在函数结束前完成,例如关闭文件、释放资源等。无论函数是否发生 panicdefer 语句都会执行。

代码语言:javascript
复制
func exampleDefer() {
    defer fmt.Println("This will be printed last")
    fmt.Println("This will be printed first")
}

输出:

代码语言:javascript
复制
This will be printed first
This will be printed last

recover

recover 用于在 panic 发生时捕获错误,从而阻止程序崩溃。recover 必须在被延迟执行的函数中使用,即在 defer 中调用。

代码语言:javascript
复制
func safeFunction() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from", r)
        }
    }()
    panic("something went wrong")
}

在这个例子中,panic 会触发,但 recover 会捕获到这个错误信息,并打印 "Recovered from something went wrong"。程序不会崩溃,会继续执行。

综合示例

代码语言:javascript
复制
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Starting the main function")
    safeFunction()
    fmt.Println("The main function continues")
}

func safeFunction() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from", r)
        }
    }()
    fmt.Println("Calling function that might panic")
    mightGoWrong()
    fmt.Println("This line will not be executed")
}

func mightGoWrong() {
    panic("something went wrong")
}

输出:

代码语言:javascript
复制
Starting the main function
Calling function that might panic
Recovered from something went wrong
The main function continues

在这个综合示例中,main 函数调用 safeFunctionsafeFunction 在调用 mightGoWrong 后触发 panic,但 recover 捕获了这个 panic 并打印错误信息,随后 main 函数继续执行。

我们之前实现了中间件机制,错误处理也可以作为一个中间件,增强 gee 框架的能力。

新增文件 gee/recovery.go,在这个文件中实现中间件 Recovery

代码语言:javascript
复制
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()
	}
}

Recovery 的实现非常简单,使用 defer 挂载上错误恢复的函数,在这个函数中调用 recover(),捕获 panic,并且将堆栈信息打印在日志中,向用户返回 Internal Server Error

你可能注意到,这里有一个 trace() 函数,这个函数是用来获取触发 panic 的堆栈信息,完整代码如下:

gee/recovery.go

代码语言:javascript
复制
func trace(message string) string {
	var pcs [32]uintptr
	n := runtime.Callers(3, pcs[:]) // skip first 3 caller

	var str strings.Builder
	str.WriteString(message + "\nTraceback:")
	for _, pc := range pcs[:n] {
		fn := runtime.FuncForPC(pc)
		file, line := fn.FileLine(pc)
		str.WriteString(fmt.Sprintf("\n\t%s:%d", file, line))
	}
	return str.String()
}

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()
	}
}

trace() 中,调用了 runtime.Callers(3, pcs[:]),Callers 用来返回调用栈的程序计数器, 第 0 个 Caller 是 Callers 本身,第 1 个是上一层 trace,第 2 个是再上一层的 defer func。因此,为了日志简洁一点,我们跳过了前 3 个 Caller。

接下来,通过 runtime.FuncForPC(pc) 获取对应的函数,在通过 fn.FileLine(pc) 获取到调用该函数的文件名和行号,打印在日志中。

至此,gee 框架的错误处理机制就完成了。

ps:代码粘贴并不是很完整,有需要的可以去作者github获取

https://github.com/geektutu/7days-golang

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • go语言之认识web框架
    • day1-handler
      • base1
      • base2
      • base3
    • day2-context
      • day3
        • 具体实现
      • day4-Group
        • day5-middlewares
          • 具体设计
        • day6-template
          • day7-error
            • panic
            • defer
            • recover
            • 综合示例
        相关产品与服务
        消息队列 TDMQ
        消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档