Dawn's Blogs

分享技术 记录成长

0%

从零实现一个Web框架 (2) 上下文

在本节,最终的代码目录结构如下:

1
2
3
4
5
6
7
dain/
|--context.go
|--dain.go
|--router.go
|--go.mod
main.go
go.mod

实现目标

最终实现得效果如下,此时更加接近于 GIN:

  • Handler 得参数变为 dain.Context,同时提供了对表单和 URL 的查询 PostForm/Query
  • dain.Context 同 GIN 一样,封装了 HTML/String/JSON/Data 函数,快速构建 HTTP 响应。

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
package main

import (
"DawnGin/dain"
"net/http"
)

func main() {
e := dain.New()

e.Get("/", func(c *dain.Context) {
c.HTML(http.StatusOK, "<h1>Hello Dawn</h1>")
})

e.Get("/hello", func(c *dain.Context) {
c.String(http.StatusOK, "Hello World, URL path = %v", c.Path)
})

e.Post("/login", func(c *dain.Context) {
c.JSON(http.StatusOK, dain.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})

e.Run(":9000")
}

构建上下文

dain/context.go

上下文 Context 即一个请求的上下文,它随着每一个请求的出现而产生,响应的结束而销毁。

Context 结构

Context 可以对一些代码进行封装,使用起来更加简便。首先看 Context 的结构:

  • dain.H 同 gin.H 的作用一样,是 map[string]interface{} 的简便写法。
  • 在 Context 中,保存了一次HTTP的请求 Req 和响应 Writer,同时保存了一些请求和响应信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type H map[string]interface{}

type Context struct {
// HTTP 请求 响应
Writer http.ResponseWriter
Req *http.Request
// 请求信息
Path string // 请求路径
Method string // 请求方法
// 响应信息
StatusCode int // 响应码
}

func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
Writer: w,
Req: r,
Path: r.URL.Path,
Method: r.Method,
}
}

获取请求数据

封装关于获取请求数据的函数 PostForm/Query

1
2
3
4
5
6
7
8
9
// PostForm 根据 key 获取第一个表单数据
func (c *Context) PostForm(key string) string {
return c.Req.FormValue(key)
}

// Query 根据 key 获取请求的 query 数据
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key)
}

改变 HTTP 响应头

封装改变 HTTP 响应的函数 Status/SetHeader

1
2
3
4
5
6
7
8
9
10
// Status 设置响应状态码
func (c *Context) Status(code int) {
c.StatusCode = code
c.Writer.WriteHeader(code)
}

// SetHeader 设置响应头
func (c *Context) SetHeader(key, value string) {
c.Writer.Header().Set(key, value)
}

快速响应

封装快速响应的函数 String/JSON/Data/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
27
28
29
30
func (c *Context) String(code int, format string, values ...interface{}) error {
c.SetHeader("Content-Type", "text/plain")
c.Status(code)
_, err := c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
return err
}

func (c *Context) JSON(code int, obj interface{}) error {
c.SetHeader("Content-Type", "application/json")
c.Status(code)
encoder := json.NewEncoder(c.Writer)
if err := encoder.Encode(obj); err != nil {
return err
}

return nil
}

func (c *Context) Data(code int, data []byte) error {
c.Status(code)
_, err := c.Writer.Write(data)
return err
}

func (c *Context) HTML(code int, html string) error {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
_, err := c.Writer.Write([]byte(html))
return err
}

路由器

dain/router.go

router 结构

于此同时,将路由器从 dain/dain.go 中分离出来。路由器的结构为:

1
2
3
4
5
6
7
8
9
type router struct {
handlers map[string]HandlerFunc
}

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

路由功能

需要稍微更改路由注册和路由功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// addRouter 实现路由注册功能
func (r *router) addRouter(method, pattern string, handler HandlerFunc) {
log.Printf("Router %v - %v\n", method, pattern)
key := method + "-" + pattern
r.handlers[key] = handler
}

// handle 实现路由功能
func (r *router) handle(c *Context) {
key := c.Method + "-" + c.Path
if handler, ok := r.handlers[key]; ok {
handler(c)
} else {
fmt.Fprintf(c.Writer, "404 NOT FOUND FOR PATH: %v", c.Path)
}
}

框架入口

dain/dain.go

与此同时,也要更改框架入口。

首先需要将 HandlerFunc 的参数改变为 Context:

1
2
// HandlerFunc handler 函数类型
type HandlerFunc func(c *Context)

改变 Engine 的内部结构,使之内嵌 router 结构体

1
2
3
4
5
6
7
8
9
10
11
type Engine struct {
// 路由器
router *router
}

// New 返回一个 Engine 指针
func New() *Engine {
return &Engine{
router: NewRouter(),
}
}

更改路由注册的内部逻辑为,调用 router 的路由注册:

1
2
3
4
// addRouter 实现路由注册功能
func (e *Engine) addRouter(method, pattern string, handler HandlerFunc) {
e.router.addRouter(method, pattern, handler)
}

最后,需要更改 http.Handler 接口函数 ServeHTTP 的实现:

1
2
3
4
5
// 实现 http.Handler 接口,自定义路由器
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := NewContext(w, r)
e.router.handle(c)
}