侧边栏壁纸
博主头像
hanlibaby博主等级

念念不忘,必有回响

  • 累计撰写 59 篇文章
  • 累计创建 92 个标签
  • 累计收到 20 条评论

Golang Web(二) 上下文

hanlibaby
2022-04-24 / 0 评论 / 0 点赞 / 34 阅读 / 7,558 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-04-26,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

前言

上一篇文章中讲了一些 GoWeb 基础知识,通过内置的 net/http 包,我们可以快速的实现一个客户端和服务端。

通过对 net/http 中的一些内容进行封装,进而实现了一个简单的具备路由表、注册、处理路由的小框架,本篇文章将更进一步对该框架进行拓展,没看过上一篇的文章的同学可先跳转观看:Golang Web 基础

目标

  1. Router 抽离出来,方便以后进行拓展
  2. 设计上下文(Context),封装 http.ResponseWriter*http.Request 等信息
  3. 提供 GetPost 请求参数的获取,JSONStringHTML 等返回类型的支持

设计 Context

针对 Web

Web 服务中,一般的都是根据请求 *http.Request,来构造出响应 http.ResponseWriter。但是 Golang 目前对这两个对象提供的接口粒度太细了,例如我们要构造一个完整的响应,那么需要考虑消息头(Header)还有消息体(Body),在 Header 里面需要包含响应状态码(StatusCode),消息类型(ContentType)等一些大部分请求都需要设置的信息。所以,如果不进行合理的封装,那么我们每次针对一个请求进行响应的话,需要编写大量冗余且无意义的代码。

因此,在常用的一些场景,需要能够构造出高效的 HTTP 响应。

下面是封装前后返回 JSON 数据时的对比:

封装之前:

data = map[string]any {
    "username": "luoweijie",
    "password": "123456",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w)
if err := encoder.Encode(data); err != nil {
    panic(err)
}

封装之后:

type User struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

c.JSON(http.StatusOK, c.BindJson(&User{}))

针对使用场景

对于封装 *http.Requesthttp.ResponseWriter 方法,简化相关接口的调用,这是设计 Context 的原因之一,而对于一个框架来说,还需要提供额外的功能。例如:解析动态路由 /hello/:username,参数 :username 的值应该放在哪呢?/hello/${userId},路由携带信息,如何进行绑定呢。再比如,后续框架需要支持中间件了,那中间件产生的信息都放在哪呢?Context 伴随这请求的出现而出现,请求的销毁而销毁,和当前请求相关联的信息都应由 Context 来承载。因此设计一个 Context ,拓展性和复杂性都留在了框架内部,对外简化了接口。

具体实现

context.go

package guru

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

type M[K string, V any] map[K]V

type Context struct {
	// request path
	Path string
	// request method
	Method string
	// response code
	StatusCode int
	Writer     http.ResponseWriter
	Request    *http.Request
}

var (
	jsonContentType   = []string{"application/json; charset = utf-8"}
	stringContentType = []string{"text/plain; charset = utf-8"}
	htmlContentType   = []string{"text/html; charset = utf-8"}
)

// NewContext is initialized a new context instance
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) BindJson(dataStruct any) any {
	decoder := json.NewDecoder(c.Request.Body)
	if err := decoder.Decode(&dataStruct); err != nil {
		panic(err)
	}
	return dataStruct
}

func (c *Context) PostForm(key string) string {
	return c.Request.FormValue(key)
}

func (c *Context) Query(key string) string {
	return c.Request.URL.Query().Get(key)
}

func (c *Context) Status(statusCode int) {
	c.StatusCode = statusCode
	c.Writer.WriteHeader(statusCode)
}

func (c *Context) WriteContentType(value []string) {
	header := c.Writer.Header()
	if val := header["Content-Type"]; len(val) == 0 {
		header["Content-Type"] = value
	}
}

func (c *Context) JSON(statusCode int, data any) {
	c.WriteContentType(jsonContentType)
	c.Status(statusCode)
	encoder := json.NewEncoder(c.Writer)
	if err := encoder.Encode(data); err != nil {
		panic(err)
	}
}

func (c *Context) String(statusCode int, format string, values ...any) {
	c.WriteContentType(stringContentType)
	c.Status(statusCode)
	if _, err := c.Writer.Write([]byte(fmt.Sprintf(format, values...))); err != nil {
		panic(err)
	}
}

func (c *Context) Data(statusCode int, data []byte) {
	c.Status(statusCode)
	if _, err := c.Writer.Write(data); err != nil {
		panic(err)
	}
}

func (c *Context) HTML(code int, html string) {
	c.WriteContentType(htmlContentType)
	c.Status(code)
	if _, err := c.Writer.Write([]byte(html)); err != nil {
		panic(err)
	}
}
  • 代码:type M[K string, V any] map[K]Vmap[string]any 起了个别名 M,后续在使用的时候更加简洁
  • 当前的 Context 对象只包含了 *http.Requesthttp.ResponseWriter,另外再提供了 MethodPath 这两个属性的访问
  • 针对 GetPost 请求分别提供了 QueryPostFormBindJson方法,其中 PostForm 用于表单提交参数,BindJson 用于接收 JSON 数据
  • 提供了构造 JSONStringHTML等响应方法

路由

将和路由相关的方法和结构提取出来,后续方便对其进行拓展,如实现动态路由。

router.go

package guru

import (
	"log"
	"net/http"
)

type router[K string, V HandleFunc] struct {
	handlers map[K]V
}

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

func (r *router[K, V]) addRoute(methodKey, pattern K, handle V) {
	log.Printf("Route %4s - %s", methodKey, pattern)
	key := methodKey + "-" + pattern
	r.handlers[key] = handle
}

func (r *router[K, V]) handle(c *Context) {
	key := c.Method + "-" + c.Path
	if handler, ok := r.handlers[K(key)]; ok {
		handler(c)
	} else {
		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
	}
}

入口

guru.go

package guru

import (
	"log"
	"net/http"
)

// HandleFunc request handler function
type HandleFunc func(*Context)

// ServeHandler A handler that accepts all requests
type ServeHandler struct {
	router *router[string, HandleFunc]
}

// New Constructor of guru.ServeHandler
func New() *ServeHandler {
	return &ServeHandler{router: NewRouter()}
}

func (serveHandler *ServeHandler) addRoute(methodKey, pattern string, handleFunc HandleFunc) {
	serveHandler.router.addRoute(methodKey, pattern, handleFunc)
}

// GET define a get request
func (serveHandler *ServeHandler) GET(pattern string, handleFunc HandleFunc) {
	serveHandler.router.addRoute("GET", pattern, handleFunc)
}

// POST define a post request
func (serveHandler *ServeHandler) POST(pattern string, handleFunc HandleFunc) {
	serveHandler.router.addRoute("POST", pattern, handleFunc)
}

func (serveHandler *ServeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	c := newContext(w, r)
	serveHandler.router.handle(c)
}

// Run define a method to start http server
func (serveHandler *ServeHandler) Run(addr string) {
	err := http.ListenAndServe(addr, serveHandler)
	if err != nil {
		log.Println("server start error")
	}
}

在将 router 相关的代码抽离之后,简洁了不少,最重要的还是实现 ServeHTTP 方法,接管了所有 HTTP 请求,在调用 router.handle 方法之前,构造了一个 Context 对象,这个对象目前还比较简单,仅包含了原来的两个参数。

使用

main.go

package main

import (
	"lwjppz.cn/guru"
	"net/http"
)

type User struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

func main() {
	server := guru.New()

	server.GET("/", func(c *guru.Context) {
		c.HTML(http.StatusOK, "<h1>Hello Guru!</h1>")
	})

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

	server.POST("/loginForm", func(c *guru.Context) {
		c.JSON(http.StatusOK, guru.M[string, any]{
			"username": c.PostForm("username"),
			"password": c.PostForm("password"),
		})
	})

	server.POST("/loginJson", func(c *guru.Context) {
		c.JSON(http.StatusOK, c.BindJson(&User{}))
	})

	server.Run(":8080")
}

测试

打开 PowerShell 测试一下:

 Mi: ~ ❯ Invoke-WebRequest http://localhost:8080/
StatusCode        : 200
StatusDescription : OK
Content           : <h1>Hello Guru!</h1>
RawContent        : HTTP/1.1 200 OK
                    Content-Length: 20
                    Content-Type: text/html; charset = utf-8
                    Date: Mon, 25 Apr 2022 01:53:16 GMT

                    <h1>Hello Guru!</h1>

 Mi: ~ ❯ Invoke-WebRequest http://localhost:8080/hello?name=luoweijie
StatusCode        : 200
StatusDescription : OK
Content           : hello luoweijie, you're at /hello

RawContent        : HTTP/1.1 200 OK
                    Content-Length: 34
                    Content-Type: text/plain; charset = utf-8
                    Date: Mon, 25 Apr 2022 01:54:02 GMT

                    hello luoweijie, you're at /hello

 Mi: ~ ❯ $Uri = 'http://localhost:8080/loginForm'
 Mi: ~ ❯ $UserForm = @{
     username = 'luoweijie'
     password = '123456'
}
 Mi: ~ ❯ Invoke-WebRequest -Uri $Uri -Method Post -Form $UserForm
StatusCode        : 200
StatusDescription : OK
Content           : {"password":"123456","username":"luoweijie"}

RawContent        : HTTP/1.1 200 OK
                    Date: Mon, 25 Apr 2022 02:13:16 GMT
                    Content-Type: application/json; charset=utf-8
                    Content-Length: 45

                    {"password":"123456","username":"luoweijie"}

 Mi: ~ ❯ $Uri = 'http://localhost:8080/loginJson'
 Mi: ~ ❯ Invoke-WebRequest -Uri $Uri -Method Post -Body '{"username": "luoweijie", "password": "123456"}'
StatusCode        : 200
StatusDescription : OK
Content           : {"username":"luoweijie","password":"123456"}

RawContent        : HTTP/1.1 200 OK
                    Date: Mon, 25 Apr 2022 02:16:48 GMT
                    Content-Type: application/json; charset=utf-8
                    Content-Length: 45

                    {"username":"luoweijie","password":"123456"}
0

评论区