Gogee_Day2 コンテキスト

Last renew: April 7, 2022 pm

注:本文仅为个人学习笔记,无任何版权。

本部分主要内容:

  1. 将router独立出来,方便之后增强。
  2. 设计context,封装Request和Response,提供对JSON(JavaScript Object Notation)、HTML等返回类型的支持。

コンテキストの設計

必要がある?

  1. 对于web服务来说,无非就是根据Request*http.Request,构造响应http.ResponseWriter。但这两个对象所提供的API太细了,比如,如果我们想要构建一个完整的Response,需要考虑Header,Body,而Header包含了状态码 (StatusCode),消息类型(ContentType) 等几乎每次请求都要设置的信息。所以如果不进行有效的封装,使用这个框架的用户需要写大量重复且复杂的代码,还容易出错。所以说对于一个好的框架而言,能够高效的构造出HTTP响应是一个关键点。
  2. 针对使用场景来说,封装*http.Requesthttp.ResponseWriter的方法,简化相关port的调用只是设计Context原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态router/hello/:name,参数:name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context可以随着每一个请求的出现而产生,也随着请求的结束而销毁。所以设计Context结构,我们可以将复杂性和扩展性留在内部,对外简化了port和router的处理函数;要实现的中间件、参数都统一使用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
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 gogee

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{}起别名gogee.H这样在构建JSON数据时更加简洁。

Context结构目前只包含http.ResponseWriter*http.Request不过同时提供了对于Method/Path等request的常用属性的直接访问。

代码提供了访问Query和PostForm参数的方法。

代码提供了快速构造String/Data/JSON/HTML响应的方法。

Router

我们将路由相关的方法和结构提取出来,放到了一个新的文件router.go中,方便我们下一次对于router的功能进行增强,比如提供动态路由的支持。同时,router的handle方法做了一个细微的调整,将handler的参数变为了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
package gogee

import (
"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) {
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并且增加了context部分之后,我们可以大幅度缩减gogee.go文件的内容。因为一些复杂的东西已经被放在router.goContext.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
package gogee

import (
"log"
"net/http"
)

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

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

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

// addRoute is the way to add something to route
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 POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}

//Run definesd the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}

// To use the ListenAndServe, we need to set a ServeHTTP struct
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := newContext(w, req)
engine.router.handle(c)
}

当然最重要的还是通过实现了ServeHTTP的接口之后,接管了所有的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 (
"gogee"
"net/http"
)

func main() {
r := gogee.New()
r.GET("/", func(c *gogee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gogee</h1>")
})

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

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

r.Run(":9999")
}