Golang | Gee 框架的设计和实现

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:9999localhost: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 有这些必要:

  1. 简化接口调用:request 和 response 提供的接口粒度太细,需要每次请求都设置相同的 Header 等信息,这在构造完整的响应时需要写大量重复的代码;
  2. 支持额外功能:如果需要解析动态路由、需要支持中间件时,直接使用 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
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值