手动实现 Go-Gin 框架

一、标准库启动 Web 服务

  • Go 语言内置了 net/http 库,封装了 HTTP 网络编程的基础的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

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

func main() {
// 两个路由绑定两个 Handler
http.HandleFunc("/", indexHandler)
http.HandleFunc("/hello", helloHandler)
// 启动 Web 服务
log.Fatal(http.ListenAndServe(":9999", nil))
}

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

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

/*
$ go run go-web/0-http-base/main.go
$ curl localhost:9999
URL.Path = "/"
$ curl localhost:9999/hello
Header["User-Agent"] = ["curl/8.0.1"]
Header["Accept"] = ["*\/*"]
*/
  • http.ListenAndServe(":9999", nil) 第一个参数是地址,表示在 9999 端口监听;第二个参数代表处理所有 HTTP 请求的实例,nil 代表使用标准库中的实例处理
  • 第二个参数就是基于 net/http 标准库实现 Web 框架的入口,类型是 Handler,是一个接口,有一个方法 ServeHTTP,源码如下:
1
2
3
4
5
6
7
package http

type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error
  • 因此只要传入任何实现了 ServerHTTP 接口的实例,所有 HTTP 请求就会交给该实例处理了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

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

// Engine is the uni handler for all requests
type Engine struct{}

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

/*
$ go run go-web/0-http-base/main-handler.go
$ curl localhost:9999
URL.Path = "/"
$ curl localhost:9999/hello
Header["User-Agent"] = ["curl/8.0.1"]
Header["Accept"] = ["*\/*"]
*/
  • 这里定义了一个空的结构体 Engine,实现了方法 ServeHTTP。这个方法有两个参数,第二个参数是 Request,包含了该 HTTP 请求的所有的信息,比如请求地址、Header 和 Body 等信息;第一个参数是 ResponseWriter,可以用其构造针对该请求的响应
  • 此时,所有的 HTTP 请求转向了自定义的处理逻辑,拥有了统一的控制入口。在这里可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等

二、搭建框架基础雏形

  • 最终的代码目录结构是这样的:
1
2
3
4
gee/
|--gee.go
main.go
go.mod

1. gee.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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)}
}

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-/helloPOST-/hello;value 是用户映射的处理方法
  • (*Engine).GET() 会将路由和处理方法注册到映射表 router
  • (*Engine).Run() 方法是 ListenAndServe 的包装
  • (*Engine).ServeHTTP() 用来解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回 404

2. main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"net/http"

"go-study/go-web/1-base/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")
}

/*
$ go run go-web/1-base/main.go
$ curl localhost:9999
URL.Path = "/"
$ curl localhost:9999/hello
Header["User-Agent"] = ["curl/8.0.1"]
Header["Accept"] = ["*\/*"]
*/
  • 使用 New() 创建 gee 的实例,使用 GET() 添加路由,最后使用 Run() 启动 Web 服务
  • 这里的路由,只是静态路由,暂时不支持 /hello/:name 这样的动态路由

三、设计 Context

  • 封装请求和响应:对 Web 服务来说,无非是根据请求 *http.Request,构造响应 http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息
  • 支撑额外的功能:比如解析动态路由 /hello/:name,或者支持中间件
  • 因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点
  • Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例

案例:返回 JSON 数据,比较封装前后的差距

封装前:

1
2
3
4
5
6
7
8
9
10
obj = map[string]interface{}{
"name": "geektutu",
"password": "1234",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w)
if err := encoder.Encode(obj); err != nil {
http.Error(w, err.Error(), 500)
}

封装后:

1
2
3
4
c.JSON(http.StatusOK, gee.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})

1. context.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package gee

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

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 响应的方法

2. router.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)
} else {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
}
}
  • 将路由相关的方法和结构提取了出来,放到了一个新的文件中 router.go,方便后续对 router 的功能进行增强,例如提供动态路由的支持
  • router 的 handle 方法作了一个重要的调整,即 handler 的参数变成了 Context

3. gee.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package gee

import (
"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) {
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)
}
  • 将 router 相关的代码独立后,gee.go 简单了不少。最重要的还是通过实现了 ServeHTTP 接口,接管了所有的 HTTP 请求

4. main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"net/http"

"go-study/go-web/2-context/gee"
)

func main() {
r := gee.New()
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
r.GET("/hello", func(c *gee.Context) {
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
})
r.POST("/login", func(c *gee.Context) {
c.JSON(http.StatusOK, gee.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})

r.Run(":9999")
}

/*
$ go run go-web/2-context/main.go
2023/08/21 15:59:34 Route GET - /
2023/08/21 15:59:34 Route GET - /hello
2023/08/21 15:59:34 Route POST - /login
$ curl -i localhost:9999
HTTP/1.1 200 OK
Content-Type: text/html
Date: Mon, 21 Aug 2023 08:03:14 GMT
Content-Length: 18
<h1>Hello Gee</h1>
$ curl localhost:9999/hello?name=lb
hello lb, you're at /hello
$ curl localhost:9999/login -X POST -d "username=lb&password=123"
{"password":"123","username":"lb"}
*/

四、前缀树路由

  • 当前是使用 map 结构存储路由表,索引非常高效,但是只能用来索引静态路由,无法支持类似于 /hello/:name 这样的动态路由
  • 动态路由有很多种实现方式,支持的规则、性能等有很大的差异。例如开源的路由实现 gorouter 支持在路由规则中嵌入正则表达式,例如 /p/[0-9A-Za-z]+,即路径中的参数仅匹配数字和字母;另一个开源实现 httprouter 就不支持正则表达式。gin 在早期的版本并没有实现自己的路由,而是直接使用了 httprouter,后来自己实现了一个版本

1. trie.go

  • 对于路由来说,最重要的当然是注册与匹配了。开发服务时,注册路由规则,映射 handler;访问时,匹配路由规则,查找到对应的 handler
  • 实现动态路由最常用的数据结构,被称为前缀树(Trie 树)。每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配
  • HTTP 请求的路径恰好是由 / 分隔的多段构成的,因此,每一段可以作为前缀树的一个节点。通过树结构查询,如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束
    • 参数匹配 ::例如 /p/:lang/doc,可以匹配 /p/c/doc/p/go/doc
    • 通配 *:例如 /static/*filepath,可以匹配 /static/fav.ico/static/js/jQuery.js,这种模式常用于静态服务器,能够递归地匹配子路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package gee

import (
"fmt"
"strings"
)

type node struct {
pattern string // 待匹配路由,如 /p/:lang
part string // 路由中的一部分,如 :lang
children []*node // 子节点,如 [doc, tutorial, intro]
isWild bool // 是否精确匹配,part 含有 : 或 * 时为 true
}

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

// 查找第一个匹配成功的子节点
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) {
if len(parts) == height {
// 只有在叶子节点才赋值pattern属性,用来后续判断是否匹配成功
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
}

// 获取所有匹配叶子节点
func (n *node) travel(list *([]*node)) {
if n.pattern != "" {
*list = append(*list, n)
}
for _, child := range n.children {
child.travel(list)
}
}

2. router.go

  • 上面实现了 Trie 树的插入与查找,接下来需要将其应用到路由中去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package gee

import (
"net/http"
"strings"
)

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
}

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"}
  • router.go 的变化比较小,比较重要的一点是,在调用匹配到的 handler 前,将解析出来的路由参数赋值给了 c.Params。这样就能够在 handler 中,通过 Context 对象访问到具体的值了

3. context.go(+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
}

func (c *Context) Param(key string) string { // +
value, _ := c.Params[key]
return value
}
  • 在 HandlerFunc 中,希望能够访问到解析的参数。因此,需要对 Context 对象增加一个属性和方法,来提供对路由参数的访问。将解析后的参数存储到 Params 中,通过 c.Param("lang") 的方式获取到对应的值

4. router_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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")
}
}

/*
matched path: /hello/:name, params['name']: geektutu
*/
func TestGetRoute(t *testing.T) {
r := newTestRouter()
n, ps := r.getRoute("GET", "/hello/geektutu")

if n == nil {
t.Fatal("nil shouldn't be returned")
}

if n.pattern != "/hello/:name" {
t.Fatal("should match /hello/:name")
}

if ps["name"] != "geektutu" {
t.Fatal("name should be equal to 'geektutu'")
}

fmt.Printf("matched path: %s, params['name']: %s\n", n.pattern, ps["name"])
}

func TestGetRoute2(t *testing.T) {
r := newTestRouter()
n1, ps1 := r.getRoute("GET", "/assets/file1.txt")
ok1 := n1.pattern == "/assets/*filepath" && ps1["filepath"] == "file1.txt"
if !ok1 {
t.Fatal("pattern shoule be /assets/*filepath & filepath shoule be file1.txt")
}

n2, ps2 := r.getRoute("GET", "/assets/css/test.css")
ok2 := n2.pattern == "/assets/*filepath" && ps2["filepath"] == "css/test.css"
if !ok2 {
t.Fatal("pattern shoule be /assets/*filepath & filepath shoule be css/test.css")
}
}

/*
1 node{pattern=/, part=, isWild=false}
2 node{pattern=/hello/:name, part=:name, isWild=true}
3 node{pattern=/hello/b/c, part=c, isWild=false}
4 node{pattern=/hi/:name, part=:name, isWild=true}
5 node{pattern=/assets/*filepath, part=*filepath, isWild=true}
*/
func TestGetRoutes(t *testing.T) {
r := newTestRouter()
nodes := r.getRoutes("GET")
for i, n := range nodes {
fmt.Println(i+1, n)
}

if len(nodes) != 5 {
t.Fatal("the number of routes shoule be 4")
}
}

5. main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"net/http"

"go-study/go-web/3-router/gee"
)

func main() {
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=geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
})

r.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})

r.GET("/assets/*filepath", func(c *gee.Context) {
c.JSON(http.StatusOK, gee.H{"filepath": c.Param("filepath")})
})

r.Run(":9999")
}

/*
$ go run go-web/3-router/main.go
$ curl localhost:9999/hello
hello , you're at /hello
$ curl localhost:9999/hello/lb
hello lb, you're at /hello/lb
$ curl localhost:9999/assets/css/main.css
{"filepath":"css/main.css"}
*/

五、分组控制

  • 分组控制是 Web 框架应提供的基础功能之一。分组指的是路由的分组,往往某一组路由需要相似的处理,如鉴权、日志、对接第三方平台等
  • 大部分情况下的路由分组,是以相同的前缀来区分的,并且支持分组的嵌套。例如 /post 是一个分组,/post/a/post/b 可以是该分组下的子分组。作用在 /post 分组上的中间件,也都会作用在子分组,子分组还可以应用自己特有的中间件

1. gee.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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 // 前缀,如 / 或 /api
parent *RouterGroup // 父组(支持分组嵌套)
middlewares []HandlerFunc // 应用在该分组上的中间件
engine *Engine // Group 需要映射路由规则,需要有访问 Router 等能力
}

Engine struct {
*RouterGroup // 将 Engine 作为最顶层的分组
groups []*RouterGroup // store all groups
router *router
}
)

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

// 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)
}
  • Engine 从某种意义上继承了 RouterGroup 的所有属性和方法,且 (*Engine).engine 是指向自己的,因此可以将和路由有关的函数,都交给 RouterGroup 实现了

2. main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"net/http"

"go-study/go-web/4-group/gee"
)

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) {
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
})
}
v2 := r.Group("/v2")
{
v2.GET("/hello/:name", func(c *gee.Context) {
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})

v2.POST("/login", func(c *gee.Context) {
c.JSON(http.StatusOK, gee.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})
}

r.Run(":9999")
}

/*
$ go run go-web/4-group/main.go
2023/08/22 11:13:28 Route GET - /index
2023/08/22 11:13:28 Route GET - /v1/
2023/08/22 11:13:28 Route GET - /v1/hello
2023/08/22 11:13:28 Route GET - /v2/hello/:name
2023/08/22 11:13:28 Route POST - /v2/login
$ curl localhost:9999/v1/hello?name=lb
hello lb, you're at /v1/hello
$ curl localhost:9999/v2/hello/lb
hello lb, you're at /v2/hello/lb
*/

六、中间件

  • 中间件简单来说就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。因此,对中间件而言,需要考虑两个比较关键的点:
    • 插入点在哪:使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 中手工调用没有多大的优势了
    • 中间件的输入是什么:中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限
  • Gee 中间件的定义与路由映射的 Handler 一致,处理的输入是 Context 对象。插入点是框架接收到请求初始化 Context 对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对 Context 进行二次加工。另外通过调用 (*Context).Next() 函数,等待执行其他的中间件或用户的 Handler。中间件可等待用户自己定义的 Handler 处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作,另外支持设置多个中间件,依次进行调用

1. context.go(+)

  • 当前框架接收到请求后,匹配路由,该请求的所有信息都保存在 Context 中。中间件也不例外,接收到请求后,应查找所有作用于该路由的中间件,保存在 Context 中,依次进行调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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{
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)
}
}

func (c *Context) Fail(code int, err string) { // +
c.index = len(c.handlers)
c.JSON(code, H{"message": err})
}
  • 当在中间件中调用 Next 方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在 Next 方法之后定义的部分
  • 假设应用了如下中间件 A 和 B,和路由映射的 Handler。c.handlers 是这样的 [A, B, Handler],调用c.Next(),最终的顺序是 part1 -> part3 -> Handler -> part 4 -> part2
1
2
3
4
5
6
7
8
9
10
func A(c *Context) {
part1
c.Next()
part2
}
func B(c *Context) {
part3
c.Next()
part4
}

2. gee.go(+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Use is defined to add middleware to the group
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middlewares = append(group.middlewares, middlewares...)
}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 通过 URL 前缀判断请求需要用到哪些中间件
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)
}

3. router.go(+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 将从路由匹配得到的 Handler 添加到 c.handlers 列表中,执行 c.Next()
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()
}

4. logger.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package gee

import (
"log"
"time"
)

func Logger() HandlerFunc {
return func(c *Context) {
t := time.Now()
c.Next()
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}

5. main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (
"log"
"net/http"
"time"

"go-study/go-web/5-middleware/gee"
)

func onlyForV2() gee.HandlerFunc {
return func(c *gee.Context) {
t := time.Now()
time.Sleep(time.Second)
c.Fail(500, "Internal Server Error")
log.Printf("[%d] %s in %v for group v2", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}

func main() {
r := gee.New()
r.Use(gee.Logger()) // global midlleware
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) {
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
}

r.Run(":9999")
}

/*
$ go run go-web/5-middleware/main.go
2023/08/22 14:47:06 Route GET - /
2023/08/22 14:47:06 Route GET - /v2/hello/:name

$ curl localhost:9999
<h1>Hello Gee</h1>

>>> log
2023/08/22 14:46:53 [200] / in 0s

$ curl localhost:9999/v2/hello/lb
{"message":"Internal Server Error"}

>>> log
2023/08/22 14:47:09 [500] /v2/hello/lb in 1.0097476s for group v2
2023/08/22 14:47:09 [500] /v2/hello/lb in 1.0097476s
*/

七、模板

  • 现在流行前后端分离的开发模式,即 Web 后端提供 RESTful 接口,返回结构化的数据(通常为 JSON 或者 XML)。前端使用 AJAX 技术请求到所需的数据,利用 JavaScript 进行渲染。这种开发模式前后端解耦,优势非常突出
  • 前后分离的一大问题在于,页面是在客户端渲染的,比如浏览器,这对于爬虫并不友好。Google 爬虫已经能够爬取渲染后的网页,但是爬取服务端直接渲染的 HTML 页面仍是主流

1. 静态文件访问

  • 要做到服务端渲染,第一步便是要支持 JS、CSS 等静态文件。之前设计动态路由时,支持通配符 * 匹配多级子路径,如果将静态文件放在某个目录下,就可以根据路由匹配的值映射到真实的文件,返回给客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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)
}

// 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)
}
}
  • 用户可以将磁盘上的某个文件夹 root 映射到路由 relativePath,例如:
1
2
3
4
r := gee.New()
r.Static("/assets", "/opt/src/static")
// 或相对路径 r.Static("/assets", "./static")
r.Run(":9999")
  • 用户访问 localhost:9999/assets/css/main.css,最终返回 /opt/src/static/css/main.css

2. HTML 模板渲染

  • Go 语言内置了 text/templatehtml/template 两个模板标准库,其中 html/template 为 HTML 提供了较为完整的支持。包括普通变量渲染、列表渲染、对象渲染等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Engine struct {
*RouterGroup
groups []*RouterGroup
router *router
// html render
htmlTemplates *template.Template // 将所有的模板加载进内存
funcMap template.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))
}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// ...
c := newContext(w, req)
c.handlers = middlewares
c.engine = engine // +
engine.router.handle(c)
}
  • 对原来的 (*Context).HTML() 方法做了些修改,使之支持根据模板文件名选择模板进行渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
type Context struct {
// ...
// engine pointer
engine *Engine // 通过 Context 访问 Engine 中的 HTML 模板
}

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())
}
}
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

import (
"fmt"
"go-study/go-web/6-template/gee"
"html/template"
"net/http"
"time"
)

type student struct {
Name string
Age int8
}

func FormatAsDate(t time.Time) string {
year, month, day := t.Date()
return fmt.Sprintf("%d-%02d-%02d", year, month, day)
}

func main() {
r := gee.New()
r.Use(gee.Logger())
r.SetFuncMap(template.FuncMap{
"FormatAsDate": FormatAsDate,
})
r.LoadHTMLGlob("D:/test/templates/*")
r.Static("/assets", "D:/test/static")

stu1 := &student{Name: "LB", Age: 20}
stu2 := &student{Name: "Jack", Age: 22}

r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "css.tmpl", nil)
})
r.GET("/students", func(c *gee.Context) {
c.HTML(http.StatusOK, "arr.tmpl", gee.H{
"title": "LB",
"stuArr": [2]*student{stu1, stu2},
})
})
r.GET("/date", func(c *gee.Context) {
c.HTML(http.StatusOK, "custom_func.tmpl", gee.H{
"title": "LB",
"now": time.Date(2019, 8, 17, 0, 0, 0, 0, time.UTC),
})
})
r.Run(":9999")
}

/*
$ go run go-web/6-template/main.go
2023/08/22 16:43:13 Route GET - /assets/*filepath
2023/08/22 16:43:13 Route GET - /
2023/08/22 16:43:13 Route GET - /students
2023/08/22 16:43:13 Route GET - /date
2023/08/22 16:43:27 [200] / in 0s
2023/08/22 16:43:27 [0] /assets/css/main.css in 113.0595ms
2023/08/22 16:43:38 [200] /date in 128.9µs
2023/08/22 16:43:54 [200] /students in 221.2µs
*/

八、错误恢复

1. panic

  • Go 语言中,比较常见的错误处理方法是返回 error,由调用者决定后续如何处理。但是如果是无法恢复的错误,可以手动触发 panic,如果在程序运行过程中出现了类似于数组越界的错误,panic 也会被触发。panic 会中止当前执行的程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
fmt.Println("before panic")
panic("crash")
fmt.Println("after panic")
}

/*
$ go run main.go
before panic
panic: crash

goroutine 1 [running]:
main.main()
D:/go-test/main.go:7 +0x65
exit status 2
*/
  • 数组越界触发的 panic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
arr := []int{1, 2, 3}
fmt.Println(arr[4])
}

/*
$ go run main.go
panic: runtime error: index out of range [4] with length 3

goroutine 1 [running]:
main.main()
D:/go-test/main.go:7 +0x1d
exit status 2
*/

2. defer

  • panic 会导致程序被中止,但是在退出前,会先处理完当前协程上已经 defer 的任务,执行完成后再退出
  • 可以 defer 多个任务,在同一个函数中 defer 多个任务,会逆序执行。即先执行最后 defer 的任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
defer func() {
fmt.Println("defer func")
}()

arr := []int{1, 2, 3}
fmt.Println(arr[4])
}

/*
$ go run main.go
defer func
panic: runtime error: index out of range [4] with length 3

goroutine 1 [running]:
main.main()
D:/go-test/main.go:11 +0x45
exit status 2
*/

3. recover

  • Go 语言提供了 recover 函数,可以避免因为 panic 发生而导致整个程序终止,recover 函数只在 defer 中生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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")
}

/*
$ go run main.go
defer func
recover success
after recover
*/

4. 框架的错误处理机制

  • 对一个 Web 框架而言,错误处理机制是非常必要的。可能是框架本身没有完备的测试,导致在某些情况下出现空指针异常等情况。也有可能用户不正确的参数,触发了某些异常,例如数组越界,空指针等。如果因为这些原因导致系统宕机,必然是不可接受的
  • 错误处理可以作为一个中间件,增强框架的能力
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package gee

import (
"fmt"
"log"
"net/http"
"runtime"
"strings"
)

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

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()
}
}
  • 使用 defer 挂载上错误恢复的函数,在这个函数中调用 recover(),捕获 panic,并且将堆栈信息打印在日志中,向用户返回 Internal Server Error
  • trace() 函数用来获取触发 panic 的堆栈信息。调用了 runtime.Callers(3, pcs[:]),Callers 用来返回调用栈的程序计数器。第 0 个 Caller 是 Callers 本身,第 1 个是上一层 trace,第 2 个是再上一层的 defer func。因此跳过了前 3 个 Caller
  • 然后通过 runtime.FuncForPC(pc) 获取对应的函数,在通过 fn.FileLine(pc) 获取到调用该函数的文件名和行号,打印在日志中
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"net/http"

"go-study/go-web/7-panic/gee"
)

func main() {
r := gee.Default()
r.GET("/", func(c *gee.Context) {
c.String(http.StatusOK, "Hello Geektutu\n")
})
// index out of range for testing Recovery()
r.GET("/panic", func(c *gee.Context) {
names := []string{"geektutu"}
c.String(http.StatusOK, names[100])
})

r.Run(":9999")
}

/*
$ go run go-web/7-panic/main.go
2023/08/22 17:19:45 Route GET - /
2023/08/22 17:19:45 Route GET - /panic
$ curl localhost:9999/panic
{"message":"Internal Server Error"}

>>> log
2023/08/22 17:20:00 runtime error: index out of range [100] with length 1
Traceback:
D:/Develop/Go/src/runtime/panic.go:884
D:/Develop/Go/src/runtime/panic.go:113
D:/go-test/main.go:17
D:/go-test/gee/context.go:39
D:/go-test/gee/recovery.go:37
D:/go-test/gee/context.go:39
D:/go-test/gee/logger.go:12
D:/go-test/gee/context.go:39
D:/go-test/gee/router.go:100
D:/go-test/gee/gee.go:93
D:/Develop/Go/src/net/http/server.go:2948
D:/Develop/Go/src/net/http/server.go:1992
D:/Develop/Go/src/runtime/asm_amd64.s:159
*/

参考