HTTP
原生 HTTP 处理
Go 语言原生自带 HTTP 的处理能力,可以使用 http 包实现简单的 http 请求处理。
原生 HTTP 处理请求方式:
/**
* @author Real
* @since 2023/11/2 22:18
*/
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", indexHandler)
http.HandleFunc("/hello", helloHandler)
log.Fatal(http.ListenAndServe(":9999", nil))
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprintf(w, "index hanlder, URL.path = %q\n", r.URL.Path)
if err != nil {
return
}
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
headers := r.Header
for index, _ := range headers {
_, err := fmt.Fprintf(w, "header[%s] = %s\n", index, headers[index])
if err != nil {
return
}
}
}
启动之后,可以访问 localhost:9999
和 localhost:9999/hello
访问对应的服务。
$ curl http://localhost:9999/
index hanlder, URL.path = "/"
$ curl http://localhost:9999/hello
header[User-Agent] = [curl/8.1.2]
header[Accept] = [*/*]
实现 Http.handler 接口
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
engine := new(Engine)
log.Fatal(http.ListenAndServe(":9999", engine))
}
// Engine is the uni handler for all requests
type Engine struct{}
// ServeHTTP implements the http.Handler interface
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
case "/hello":
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
default:
fmt.Fprintf(w, "404 NOT FOUND: %s\n", r.URL)
}
}
在这个里面,定义了一个 Engine 空结构体,实现了 HTTP 接口的 ServeHTTP 接口。这样就可以处理所有的请求了。第一个参数是 ResponseWriter,可以利用 ResponseWriter 构造针对该请求的响应。第二个参数是 Request,该对象包含了该 HTTP 请求的所有信息,比如请求地址、Header、Body 等信息。
在 main 函数中,给 ListenAndServe 方法的第二个参数传入了刚才创建的 engine 实例。这样就将所有的 HTTP 请求转向了我们自己的处理逻辑。在实现 Engine 之前,可以调用 http.HandleFunc 实现了路由和 Handler 的映射,也就是只能针对具体的路由写处理逻辑。但是在实现 Engine 之后就可以拦截所有的 HTTP 请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。这里的代码的运行结果与之前的是一致的。
Gin 的处理方式
import (
"fmt"
"net/http"
)
// HandlerFunc defines the request handler used by gee_module
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
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
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-/hello、POST-/hello,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法 Handler,value 是用户映射的处理方法。
- 当用户调用
(*Engine).GET()
方法时,会将路由和处理方法注册到映射表 router 中,(*Engine).Run()
方法是ListenAndServe
的包装。 - Engine 实现的 ServeHTTP 方法的作用就是,解析请求的路径,查找路由映射表。如果查到,就执行注册的处理方法。如果查不到,就返回 404 NOT FOUND。
Context
封装前的请求
如果不对 Gee 的请求方法做封装,那么每次使用的时候,都是直接将 request 和 Response 作为匿名函数的参数。
func main() {
engine := gee.New()
engine.GET("/", func(writer http.ResponseWriter, request *http.Request) {
})
}
这样的使用看起来会很臃肿,而且和使用原生的 http 包提供的方法,没有什么太大的区别。这个时候就需要将请求和返回参数做一些封装,顺带也支持更强大的功能。
因此,封装 Context 有这些必要:
- 简化接口调用:request 和 response 提供的接口粒度太细,需要每次请求都设置相同的 Header 等信息,这在构造完整的响应时需要写大量重复的代码;
- 支持额外功能:如果需要解析动态路由、需要支持中间件时,直接使用 Request 和 Response 接口,对于全局的请求来说不具备复用性。
Context 实现
封装 Context 的具体实现如下。
package gee
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
const ContentType = "Content-Type"
// H 构造 JSON 数据的别名
type H map[string]interface{}
type Context struct {
// 原始对象
Writer http.ResponseWriter
Request *http.Request
// 请求信息
Path string
Method string
// 返回信息
StatusCode int
}
func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
Writer: w,
Request: r,
Path: r.URL.Path,
Method: r.Method,
}
}
// ---------- 获取请求值 ----------
func (c *Context) GetPostFormVal(key string) string {
return c.Request.FormValue(key)
}
func (c *Context) GetQueryVal(key string) string {
return c.Request.URL.Query().Get(key)
}
// ---------- 设置返回值 ----------
func (c *Context) SetStatusCode(code int) {
c.StatusCode = code
c.Writer.WriteHeader(code)
}
func (c *Context) SetHeader(key, value string) {
c.Request.Header.Set(key, value)
}
// ---------- 设置返回值类型 ----------
func (c *Context) String(code int, format string, values ...interface{}) {
c.SetHeader(ContentType, "text/plain")
c.SetStatusCode(code)
_, err := c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
if err != nil {
log.Printf("Response String Error: %v", err)
}
}
func (c *Context) JSON(code int, data interface{}) {
c.SetHeader(ContentType, "application/json")
c.SetStatusCode(code)
encoder := json.NewEncoder(c.Writer)
if err := encoder.Encode(data); err != nil {
log.Printf("Response JSON Error: %v", err)
http.Error(c.Writer, err.Error(), 500)
}
}
func (c *Context) HTML(code int, html string) {
c.SetHeader(ContentType, "text/html")
c.SetStatusCode(code)
if _, err := c.Writer.Write([]byte(html)); err != nil {
log.Printf("Response HTML Error: %v", err)
}
}
func (c *Context) Data(code int, data []byte) {
c.SetStatusCode(code)
if _, err := c.Writer.Write(data); err != nil {
log.Printf("Response Data Error: %v", err)
}
}
Context 的封装实现,主要提供了这些特性:
- 提供了快速构造 Map 类型数据的方法别名;
- 提供了接收 Request 和 Response 两个参数的快速构造方法;
- 提供了获取请求中携带的参数的方法、设置返回值以及 Header 属性;
- 提供了向返回值中写入常见的 String、JSON、Data、HTML 的方法;
Router
前面参考 Gin 的实现方式,编写了一个简单的 Gee 框架的入口。之前是将 Router、Engine 等功能全部耦合在一起,如果要在路由阶段提供更多功能,将 Router 独立出来会是更好的选择。
抽离 Router
将之前实现的 Router 相关的功能单独剥离,得到下面的实现。
package gee
import (
"log"
"net/http"
)
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) {
log.Printf("[Route]: %4s - %s", method, pattern)
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)
return
}
// not found, return 404
c.String(http.StatusNotFound, "[Route]: 404 NOT FOUND: %s\n", c.Path)
}
处理 Gee 文件
抽离了 Router 之后,原文件 Gee 中的一些方法也需要同步修改。
package gee
import (
"net/http"
)
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(c *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) {
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) {
context := NewContext(w, req)
engine.router.handle(context)
}
这样处理之后,Gee 框架的结构就更趋合理了。
│ go.mod
├─gee
│ context.go
│ gee.go
│ router.go
├─gee_context
│ └─main
│ main.go
└─http_base
├─gee_module
│ └─main
│ main.go
├─http_handler
│ http_base.go
└─serve_http
serve_http.go
测试使用
添加测试 case 以及测试的实现代码。
package gee_context
import (
"gee/gee"
"net/http"
"testing"
)
func TestContext(t *testing.T) {
r := gee.New()
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
r.GET("/hello", func(c *gee.Context) {
// expect /hello?name=Real
c.String(http.StatusOK, "hello %s, you're at %s\n", c.GetQueryVal("name"), c.Path)
})
r.POST("/login", func(c *gee.Context) {
c.JSON(http.StatusOK, gee.H{
"username": c.GetPostFormVal("username"),
"password": c.GetPostFormVal("password"),
})
})
if err := r.Run(":9999"); err != nil {
t.Fatalf("Error, exit. error: %v", err)
}
}
运行结果:
- 测试
/hello
接口,可以得到正常响应。
- 测试
/login
接口,同样可以得到正常响应。
- 测试其他接口,会正常返回 404 的结果。
前缀树路由 Router
Trie 树定义
至此为止,Gee 使用的是简单的 map 结构存储路由表。使用 map 存储键值对索引非常高效,但是缺点也很明显,只能用来索引静态路由。如果想要索引动态路由,需要借助一些其他方式。
动态路由:一条路由规则可以匹配某一类型而非某一条固定单一的路由。例如
/hello/:name
,可以匹配/hello/tom
、/hello/jack
等。
动态路由有很多种实现方式,支持的规则、性能等有很大的差异。例如开源的路由实现 gorouter 支持在路由规则中嵌入正则表达式,例如 /p/[0-9A-Za-z]+
,即路径中的参数仅匹配数字和字母;另一个开源实现 httprouter 就不支持正则表达式。gin 在早期的版本并没有实现自己的路由,而是直接使用了 httprouter,但是后期放弃了 httprouter 转而自己实现了一个版本。
实现动态路由最常用的数据结构,是前缀树 Trie。
HTTP 请求的路径恰好是由/分隔的多段构成的,因此每一段可以作为前缀树的一个节点。我们通过树结构查询,如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束。
实现动态路由之前,我们需要定义好动态路由需要支持的功能:
- 参数匹配:例如
/p/:lang/doc
,可以匹配/p/python/doc
和/p/go/doc
。 - 支持通配符:例如
/static/*filepath
,可以匹配/static/fav.ico
,也可以匹配/static/js/jQuery.js
。用于静态服务器的时候,需要支持递归地匹配子路径。
Trie 树实现
路由最重要的功能是注册和匹配。Trie 树中每个节点,我们定义为下面这种结构。
type Node struct {
Pattern string // 待匹配的路由,例如 /p/:lang,将参数进行上下文传递
Part string // 待匹配路由的一部分,例如 :lang
Children []*Node // 子节点,例如 :lang 的可选项 [go, java, php] 等
IsFuzzy bool // 是否模糊匹配,part 含有 : 或者 * 时为 true
}
以 /p/:lang/doc
路由为例,我们需要在路由 Trie 树中插入这个路由。服务开发时,我们注册路由规则映射 Handler;服务访问时匹配路由规则,找到对应的 Handler。因此 Trie 树需要支持节点的插入与查询。
Trie 树的插入功能很简单,只需要递归查找每一层的节点,如果没有匹配到当前 part 的节点,新建一个即可。/p/:lang/doc
只有在第三层节点,即 doc 节点,pattern 才会设置为 /p/:lang/doc
。p 和 :lang
节点的 pattern 属性皆为空。因此匹配结束时可以用 n.Pattern == ""
来判断路由规则是否匹配成功。例如,/p/python
虽能成功匹配到 :lang
,但 :lang
的 pattern 值为空,因此匹配失败。查询功能,同样也是递归查询每一层的节点,退出规则是,匹配到了 *
,匹配失败,或者匹配到了第 len(parts)
层节点。
按照这个思路,完整的 Trie 路由树的实现如下。
package gee
import (
"fmt"
"strings"
)
type Node struct {
Pattern string // 待匹配的路由,例如 /p/:lang,将参数进行上下文传递
Part string // 待匹配路由的一部分,例如 :lang
Children []*Node // 子节点,例如 :lang 的可选项 [go, java, php] 等
IsFuzzy bool // 是否模糊匹配,part 含有 : 或者 * 时为 true
}
// MatchChild 获取下一层第一个匹配的路由
func (n *Node) MatchChild(part string) *Node {
for _, child := range n.Children {
if child.Part == part || child.IsFuzzy {
return child
}
}
return nil
}
// MatchChildren 匹配下一层中所有匹配的子路由
func (n *Node) MatchChildren(part string) []*Node {
nodes := make([]*Node, 0)
for _, child := range n.Children {
if child.Part == part || child.IsFuzzy {
nodes = append(nodes, child)
}
}
return nodes
}
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, IsFuzzy: part[0] == ':' || part[0] == '*'}
n.Children = append(n.Children, child)
}
child.Insert(pattern, parts, height+1)
}
// Search 搜索节点下低于此高度的、且符合 Part 的部分
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
}
func (n *Node) travel(list *[]*Node) {
if n.Pattern != "" {
*list = append(*list, n)
}
for _, child := range n.Children {
child.travel(list)
}
}
func (n *Node) String() string {
return fmt.Sprintf("Node{Pattern=%s, part=%s, isWild=%t}", n.Pattern, n.Pattern, n.IsFuzzy)
}
使用 Trie 树实现的 Router
有了 Trie 树,接下来就可以使用 Trie 来代替 map 完成动态路由了。
这里使用 roots 来存储每种请求方式的 Trie 树根节点,使用 handlers 来存储每种请求方式的 HandlerFunc。此外,还提供了 getRoute 函数用来解析 :
和 *
两种通配符的参数,返回一个 map[string]string
类型的数据。
补充实现之后,router.go 文件如下。
package gee
import (
"log"
"net/http"
"strings"
)
type Router struct {
Roots map[string]*Node // 根节点,由方法确定
Handlers map[string]HandlerFunc // 对应的 HandlerFunc
}
// roots key eg, roots['GET'] roots['POST']
// handlers key eg, handlers['GET-/p/:lang/doc'], handlers['POST-/p/book']
func NewRouter() *Router {
return &Router{
Roots: make(map[string]*Node),
Handlers: make(map[string]HandlerFunc),
}
}
// parsePattern 只允许路由中有一个 * 通配符,解析出每一块 / 分割的值
func parsePattern(pattern string) []string {
patternPaths := strings.Split(pattern, "/")
parts := make([]string, 0, len(patternPaths))
for _, part := range patternPaths {
// 路径为空
if len(part) == 0 {
continue
}
parts = append(parts, part)
if strings.HasPrefix(part, "*") {
break
}
}
return parts
}
func (r *Router) addRoute(method string, pattern string, handler HandlerFunc) {
method = strings.ToUpper(method)
log.Printf("[Route]: %4s - %s", method, pattern)
parts := parsePattern(pattern)
key := method + "-" + pattern
if _, ok := r.Roots[method]; !ok {
// 没有该类型的方法,新建节点
r.Roots[method] = &Node{}
}
// 添加路由与 HandlerFunc
r.Roots[method].Insert(pattern, parts, 0)
r.Handlers[key] = handler
}
func (r *Router) getRoute(method string, path string) (node *Node, params map[string]string) {
method = strings.ToUpper(method)
root, ok := r.Roots[method]
if !ok {
return nil, nil
}
searchParts := parsePattern(path)
node = root.Search(searchParts, 0)
if node == nil {
return nil, nil
}
params = make(map[string]string)
parts := parsePattern(node.Pattern)
for index, part := range parts {
if strings.HasPrefix(part, ":") {
params[part[1:]] = searchParts[index]
}
if strings.HasPrefix(part, "*") && len(part) > 1 {
params[part[1:]] = strings.Join(searchParts[index:], "/")
break
}
}
return node, params
}
func (r *Router) handle(c *Context) {
node, params := r.getRoute(c.Method, c.Path)
if node == nil {
// not found, return 404
c.String(http.StatusNotFound, "[Route]: 404 NOT FOUND: %s\n", c.Path)
}
key := c.Method + "-" + c.Path
c.Params = params
handlerFunc := r.Handlers[key]
handlerFunc(c)
}
里面主要的改动是新增 addRoute 以及 getRoute 方法,修改了 handle 方法。
- 新增 addRoute 方法:新增
/path
的同时往 tire 中插入路径; - 新增 getRoute 方法:获取节点和解析出的请求参数;
- 修改 handle 方法:路由查找的过程交给了 trie 处理;
单元测试
可以在同一个文件夹下创建 router 的单元测试。
package gee
import (
"fmt"
"reflect"
"testing"
)
func newTestRouter() *Router {
r := NewRouter()
r.addRoute("GET", "/", nil)
r.addRoute("GET", "/hello/:name", nil)
r.addRoute("GET", "/hello/b/c", nil)
r.addRoute("GET", "/hi/:name", nil)
r.addRoute("GET", "/assets/*filepath", nil)
return r
}
func TestParsePattern(t *testing.T) {
ok := reflect.DeepEqual(parsePattern("/p/:name"), []string{"p", ":name"})
ok = ok && reflect.DeepEqual(parsePattern("/p/*"), []string{"p", "*"})
ok = ok && reflect.DeepEqual(parsePattern("/p/*name/*"), []string{"p", "*name"})
if !ok {
t.Fatal("test parsePattern failed")
}
}
func TestGetRoute(t *testing.T) {
r := newTestRouter()
n, ps := r.getRoute("GET", "/hello/Real")
if n == nil {
t.Fatal("nil shouldn't be returned")
}
if n.Pattern != "/hello/:name" {
t.Fatal("should match /hello/:name")
}
if ps["name"] != "Real" {
t.Fatal("name should be equal to 'Real'")
}
fmt.Printf("matched path: %s, params['name']: %s\n", n.Pattern, ps["name"])
}
测试通过,即表示没有问题。
分组路由 Group
分组分析
分组路由功能是现在的 web 框架普遍支持的功能之一。
如果没有路由分组,我们需要针对每一个路由进行控制,会非常冗余。有了路由分组,就能以 group 为单位对路由进行控制。真实的使用场景中,也往往都是使用分组对路由进行控制的,这样能使得路由控制更加简单。
大部分情况下的路由分组,是以相同的前缀来区分的。中间件 middleware 的应用,可以为分组控制带来更明显的收益,并非共享相同的路由前缀那么简单。
中间件是应用在分组上的,所以分组路由还需要存储应用在该分组上的中间件 middlewares。分组路由更多是为了控制路由,那么使用 engine 来对分组内路由控制会更方便,所以分组路由中应该有 engine 对象。
分组结构
根据以上分析得出,分组路由的结构如下。
type RouterGroup struct {
prefix string
middlewares []HandlerFunc // support middleware
parent *RouterGroup // support nesting
engine *Engine // all groups share an Engine instance
}
为了进一步抽象,顶级分组路由应该是 /
,也是整个项目的最顶级部分。所以 engine 也会拥有分组路由的能力。
// Engine implement the interface of ServeHTTP
type Engine struct {
*RouterGroup
router *Router
groups []*RouterGroup
}
接下来就可以将所有和路由有关的函数,都交给 RouterGroup 来实现了。
package gee
import "log"
type RouterGroup struct {
prefix string
middlewares []HandlerFunc // support middleware
parent *RouterGroup // support nesting
engine *Engine // all groups share an Engine instance
}
// Group is defined to create a new RouterGroup
// all groups share the same Engine instance
func (group *RouterGroup) Group(prefix string) *RouterGroup {
engine := group.engine
nextGroup := &RouterGroup{
prefix: group.prefix + prefix,
parent: group,
engine: engine,
}
engine.groups = append(engine.groups, nextGroup)
return nextGroup
}
func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
pattern := group.prefix + comp
log.Printf("[RouteGroup] %4s - %s", method, pattern)
group.engine.addRoute(method, pattern, handler)
}
// 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)
}
使用案例
根据上面的实现,我们可以按照下面这种方式使用路由分组功能。
package main
import (
"gee/gee"
"net/http"
)
func main() {
r := gee.New()
r.GET("/index", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Index Page</h1>")
})
v1 := r.Group("/v1")
{
v1.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
v1.GET("/hello", func(c *gee.Context) {
// expect /hello?name=Real
c.String(http.StatusOK, "hello %s, you're at %s\n", c.GetQueryVal("name"), c.Path)
})
}
v2 := r.Group("/v2")
{
v2.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/Real
c.String(http.StatusOK, "hello %s, you're at %s\n", c.GetParamVal("name"), c.Path)
})
v2.POST("/login", func(c *gee.Context) {
c.JSON(http.StatusOK, gee.H{
"username": c.GetPostFormVal("username"),
"password": c.GetPostFormVal("password"),
})
})
}
r.Run(":9999")
}
直接测试:
- 测试 POST 请求:
curl --request POST \
--url http://localhost:9999/v2/login \
--header 'content-type: multipart/form-data' \
--form username=real \
--form password=123456
中间件 Middleware
中间件 Middleware 是非业务的技术类组件。
Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。但是优秀的框架应该提供扩展的机制,允许用户自定义一些扩展,嵌入到框架中执行。这种独立于业务之外的支持内嵌的组件,就是 web 框架的中间件。
中间件设计
Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是 Context 对象。插入点是框架接收到请求初始化 Context 对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对 Context 进行二次加工。另外通过调用 (*Context).Next()
函数,中间件可等待用户自己定义的 Handler 处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作。
我们希望最终能够支持如下定义的中间件,c.Next()
表示等待执行其他的中间件或用户的 Handler。
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))
}
}
中间件在 web 框架中的机制,有点类似于 Java 中的 AOP 增强。
- 两者同样可以决定对目标进行前置、后置、环绕增强。对于目标函数的调用,在 Java 中使用 proceed 函数,在 go 的 web 框架中通常会定义一个 Next 方法。
- 不同点在于,Go 的 web 框架要实现环绕增强,需要定义一个处理器切片,在 Next 中执行目标函数前后,分别定义不同的中间件,实际上是多个中间件。
具体实现,可以在目标函数的调用前后,执行一些增强的逻辑。
中间件实现
为了方便实现,给 Context 加了两个参数。
type Context struct {
// 原始对象
Writer http.ResponseWriter
Request *http.Request
// 请求信息
Path string
Method string
Params map[string]string
// 返回信息
StatusCode int
// 中间件
handlers []HandlerFunc
index int
}
func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
Writer: w,
Request: r,
Path: r.URL.Path,
Method: r.Method,
Params: make(map[string]string),
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 (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)
}
新增的 index 是记录当前执行到第几个中间件,当在中间件中调用 Next 方法时,控制权交给了下一个中间件,直到调用到最后一个中间件;然后再从后往前,调用每个中间件在 Next 方法之后定义的部分。
因为中间件通常用于路由分组,因此 group 也需要同步修改。
// Use is defined to add middleware to the group
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middlewares = append(group.middlewares, middlewares...)
}
对于原有的 route 调用,方式也有一些变化。handle
函数中,将从路由匹配得到的 Handler
添加到 c.handlers
列表中,执行 c.Next()
。
func (r *Router) handle(c *Context) {
node, params := r.getRoute(c.Method, c.Path)
if node != nil {
key := c.Method + "-" + node.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()
}
使用案例
有了中间件 Middleware,就可以在定义 Engine 之后,将事先定义好的中间件使用到 Engine 上。
定义一个 Logger 中间件,用于在路由处理前后打印一些关键日志。
package gee
import (
"log"
"time"
)
// Logger global middleware
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.Request.RequestURI, time.Since(t))
}
}
对于 Logger 的使用很简单,只需要 Engine 调用 Use 方法即可。
package main
import (
"gee/gee"
"log"
"net/http"
"time"
)
func onlyForV2() gee.HandlerFunc {
return func(c *gee.Context) {
// Start timer
t := time.Now()
// if a server error occurred
c.Fail(500, "Internal Server Error")
// Calculate resolution time
log.Printf("[%d] %s in %v for group v2", c.StatusCode, c.Request.RequestURI, time.Since(t))
}
}
func main() {
r := gee.New()
// global middleware
r.Use(gee.Logger())
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
v2 := r.Group("/v2")
v2.Use(onlyForV2()) // v2 group middleware
{
v2.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/Real
c.String(http.StatusOK, "hello %s, you're at %s\n", c.GetParamVal("name"), c.Path)
})
}
r.Run(":9999")
}
模版 Template
模版的功能,是需要实现静态资源服务、支持 HTML 模版渲染。
现在前后端分离的开发模式中,大多数渲染功能交由前端浏览器实现。前后端渲染的一大问题是页面都是在客户端渲染的,这对于浏览器的 SEO 并不友好。在一段时间内,服务器直接渲染的 HTML 页面仍然是市面上的主流。
静态文件
要做到服务端渲染,第一步便是要支持 JS、CSS 等静态文件。之前设计的动态路由,需要支持通配符*
匹配多级子路径。这一点其实就是将静态文件完成映射,路径上的文件,匹配预先设置好的服务器上存储的路径。
找到文件之后,将文件返回,这一步 net/http
库已经实现了。因此 gee 框架要做的,仅仅是解析请求的地址,映射到服务器上文件的真实地址,交给 http.FileServer
处理就好了。
// 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.GetParamVal("filepath")
// Check if file exists and/or if we have permission to access it
if _, err := fs.Open(file); err != nil {
c.StatusCode = http.StatusNotFound
return
}
fileServer.ServeHTTP(c.Writer, c.Request)
}
}
// Static 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)
}
模版渲染
Go 语言内置了 text/template
和 html/template
两个模板标准库,其中 html/template
为 HTML 提供了较为完整的支持,包括普通变量渲染、列表渲染、对象渲染等。gee 框架的模板渲染直接使用了 html/template
提供的能力。
为了支持模版渲染,可以在 Engine 中添加对模版支持的元素。
// Engine implement the interface of ServeHTTP
type Engine struct {
*RouterGroup
router *Router
groups []*RouterGroup
htmlTemplates *template.Template // for html render
funcMap template.FuncMap // for html render
}
首先为 Engine 示例添加了 *template.Template
和 template.FuncMap
对象,前者将所有的模板加载进内存,后者是所有的自定义模板渲染函数。
另外,给用户分别提供了设置自定义渲染函数 funcMap 和加载模板的方法。
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))
}
此外,还需要对原来的 (*Context).HTML()
方法做了些小修改,使之支持根据模板文件名选择模板进行渲染。在 Context 中定义好 Engine 这样的成员变量,就能通过 Context 访问 Engine 中的 HTML 模版了。
type Context struct {
// 原始对象
Writer http.ResponseWriter
Request *http.Request
// 请求信息
Path string
Method string
Params map[string]string
// 返回信息
StatusCode int
// 中间件
handlers []HandlerFunc
index int
// 引擎指针
engine *Engine
}
func (c *Context) HTML(code int, name string, data interface{}) {
c.SetHeader(ContentType, "text/html")
c.SetStatusCode(code)
if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil {
log.Printf("Response HTML Error: %v", err)
c.Fail(http.StatusInternalServerError, err.Error())
}
}
Context 中的 engine 元素同样需要在适当的时候赋值,于是对 gee 中的 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
c.engine = engine
engine.router.handle(c)
}
错误修复 Recovery
panic
Go 语言中,比较常见的错误处理方法是返回 error,由调用者决定后续如何处理。但是如果是无法恢复的错误,可以手动触发 panic,panic 会中止当前执行的程序。
defer
panic 会导致程序被中止,但是在退出前,会先处理完当前协程上已经 defer 的任务,执行完成后再退出。效果类似于 Java 中的 try...catch...finally
语句。
recover
Go 语言还提供了 recover 函数,可以避免因为 panic 发生而导致整个程序终止,recover 函数只在 defer 中生效。
// hello.go
func test_recover() {
defer func() {
fmt.Println("defer func")
if err := recover(); err != nil {
fmt.Println("recover success")
}
}()
arr := []int{1, 2, 3}
fmt.Println(arr[4])
fmt.Println("after panic")
}
func main() {
test_recover()
fmt.Println("after recover")
}
错误处理机制
对一个 Web 框架而言,错误处理机制是非常必要的。
可能是框架本身没有完备的测试,导致在某些情况下出现空指针异常等情况。也有可能用户不正确的参数,触发了某些异常,例如数组越界,空指针等。如果因为这些原因导致系统宕机,必然是不可接受的。
为了使框架更加健壮,需要在 gee 中添加错误处理机制。
错误机制设计很简单,即在 panic 发生时,向用户返回 Internal Server Error,并且在日志中打印必要的错误信息,方便进行错误定位。
func Recovery() HandlerFunc {
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
message := fmt.Sprintf("panic occurred: %s", err)
log.Printf("%s\n\n", trace(message))
c.Fail(http.StatusInternalServerError, "Internal Server Error")
}
}()
c.Next()
}
}
可以很简单的利用之前的 Middleware 机制来实现错误的处理。使用 defer 挂载上错误恢复的函数,在这个函数中调用 recover()
方法来捕获 panic,并且将堆栈信息打印在日志中,向用户返回 Internal Server Error。
这里的 trace()
函数实现如下。
// trace print stack trace for debug
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()
}
在 trace()
中,调用了 runtime.Callers(3, pcs[:])
,Callers 用来返回调用栈的程序计数器, 第 0 个 Caller 是 Callers 本身,第 1 个是上一层 trace,第 2 个是再上一层的 defer func。因此,为了日志简洁一点,我们跳过了前 3 个 Caller。
接下来,通过 runtime.FuncForPC(pc)
获取对应的函数,在通过 fn.FileLine(pc)
获取到调用该函数的文件名和行号,打印在日志中。至此,gee 框架的错误处理机制就完成了。
其他
为了方便测试和使用,通常会定义一个默认的 Engine 提供使用。
// Default use Logger() & Recovery middlewares
func Default() *Engine {
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
可以用 Default 方法很轻易构造出一个最基础的 Engine 对象。
package main
import (
"gee/gee"
"net/http"
)
func main() {
r := gee.Default()
r.GET("/", func(c *gee.Context) {
c.String(http.StatusOK, "Hello Real\n")
})
// index out of range for testing Recovery()
r.GET("/panic", func(c *gee.Context) {
names := []string{"Real"}
c.String(http.StatusOK, names[100])
})
r.Run(":9999")
}
访问对应的 URL,可以看到访问结果。
对应的在控制台中,可以看到相同的错误的 trace 日志。
错误的发生位置与打印的日志完全吻合,符合开发的需求。