Gin框架入门

本文是对开启博客之路 | Go 语言编程之旅 (eddycjy.com)的学习。

详细分析:Go框架解析:gin - TIGERB

Gin框架启动的一个简单HTTP服务器;

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "pong"})
	})
	r.Run()
}

运行结果如下:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
...
[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

对运行结果做一个初步的概括分析,分为以下四大块:

  • 默认 Engine 实例:当前默认使用了官方所提供的 Logger 和 Recovery 中间件创建了 Engine 实例。
  • 运行模式:当前为调试模式,并建议若在生产环境时切换为发布模式。
  • 路由注册:注册了 GET /ping 的路由,并输出其调用方法的方法名。
  • 运行信息:本次启动时监听 8080 端口,由于没有设置端口号等信息,因此默认为 8080。

分析

整体流程,如下图所示:

gin.Default

func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}
  • debugPrintWARNINGDefault():调试模式日志输出

    • func debugPrintWARNINGDefault() {
      	if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
      		debugPrint(`[WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.
      
      `)
      	}
      	debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
      
      `)
      }
      
    • 在调用 debugPrintWARNINGDefault 方法时,会检查 Go 版本是否达到 gin 的最低要求,再进行调试的日志 [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. 的输出,用于提醒开发人员框架内部已经默认检查和集成了缺省值。

  • 通过调用 gin.Default 方法来创建默认的 Engine 实例,它会在初始化阶段就引入 Logger 和 Recovery 中间件,能够保障你应用程序的最基本运作,这两个中间件具有以下作用:

    • Logger:输出请求日志,并标准化日志的格式。
    • Recovery:异常捕获,也就是针对每次请求处理进行 recovery 处理,防止因为出现 panic 导致服务崩溃,并同时标准化异常日志的格式。

gin.New

New 方法返回的是HTTP服务的核心引擎;

func New() *Engine {
	debugPrintWARNINGNew()	// 调试模式日志输出 
	engine := &Engine{
		RouterGroup: RouterGroup{	// 给框架实例绑定上一个路由组
			Handlers: nil,	// engine.Use 注册的中间方法到这里
			basePath: "/",
			root:     true,	// 是否是路由根节点
		},
		FuncMap:                template.FuncMap{},
		RedirectTrailingSlash:  true,
		RedirectFixedPath:      false,
		HandleMethodNotAllowed: false,
		ForwardedByClientIP:    true,
		AppEngine:              defaultAppEngine,
		UseRawPath:             false,
		RemoveExtraSlash:       false,
		UnescapePathValues:     true,
		MaxMultipartMemory:     defaultMultipartMemory,
		trees:                  make(methodTrees, 0, 9),	// 路由树,路由最终注册到了这里
		delims:                 render.Delims{Left: "{{", Right: "}}"},
		secureJsonPrefix:       "while(1);",
	}
  // RouterGroup绑定engine自身的实例
	// 不太明白为何如此设计,职责分明么?
	engine.RouterGroup.engine = engine
	engine.pool.New = func() interface{} {	// 绑定从实例池获取上下文的闭包方法
		return engine.allocateContext()	// 获取一个Context实例
	}
	return engine
}
  • 为什么叫gin呢?这个框架实例的结构体实际命名是Engine,很明显gin就是一个很个性的简称了。Go框架解析:gin - TIGERB
  • RouterGroup:路由组,所有的路由规则都由 *RouterGroup 所属的方法进行管理,在 gin 中和 Engine 实例形成一个重要的关联组件。
  • RedirectTrailingSlash:是否自动重定向,如果启用了,在无法匹配当前路由的情况下,则自动重定向到带有或不带斜杠的处理程序去。例如:当外部请求了 /tour/ 路由,但当前并没有注册该路由规则,只有 /tour 的路由规则时,将会在内部进行判定,若是 HTTP GET 请求,将会通过 HTTP Code 301 重定向到 /tour 的处理程序去,但若是其他类型的 HTTP 请求,那么将会是以 HTTP Code 307 重定向。,通过指定的 HTTP 状态码重定向到 /tour 路由的处理程序去。
  • RedirectFixedPath:是否尝试修复当前请求路径,也就是在开启的情况下,gin 会尽可能的帮你找到一个相似的路由规则并在内部重定向过去,主要是对当前的请求路径进行格式清除(删除多余的斜杠)和不区分大小写的路由查找等。
  • HandleMethodNotAllowed:判断当前路由是否允许调用其他方法,如果当前请求无法路由,则返回 Method Not Allowed(HTTP Code 405)的响应结果。如果无法路由,也不支持重定向其他方法,则交由 NotFound Hander 进行处理。
  • ForwardedByClientIP:如果开启,则尽可能的返回真实的客户端 IP,先从 X-Forwarded-For 取值,如果没有再从 X-Real-Ip。(不理解
  • UseRawPath:如果开启,则会使用 url.RawPath 来获取请求参数,不开启则还是按 url.Path 去获取。(不理解
  • UnescapePathValues:是否对路径值进行转义处理。
  • MaxMultipartMemory:相对应 http.Request ParseMultipartForm 方法,用于控制最大的文件上传大小。
  • trees:多个压缩字典树(Radix Tree),每个树都对应着一种 HTTP Method。你可以理解为,每当你添加一个新路由规则时,就会往 HTTP Method 对应的那个树里新增一个 node 节点,以此形成关联关系。
  • delims:用于 HTML 模板的左右定界符。

r.GET

// 注册GET请求路由
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	// 往路由组内 注册GET请求路由
	return group.handle("GET", relativePath, handlers)
}合并现有和新注册的 Handler,并创建一个函数链 HandlersChain。

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := group.calculateAbsolutePath(relativePath)
	// 把中间件的handle和该路由的handle合并
	handlers = group.combineHandlers(handlers)
	// 注册一个GET集合的路由
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}
  • handlers ...HandlerFunc:类型前面三个点,表示可边长参数,实际类型是切片。

  • 计算路由的绝对路径,也就是 group.basePath 与我们定义的路由路径组装,那么 group 又是什么东西呢,实际上在 gin 中存在组别路由的概念,这个知识点在后续实战中会使用到。

  • 合并现有和新注册的 Handler,并创建一个函数链 HandlersChain

  • 将当前注册的路由规则(含 HTTP Method、Path、Handlers)追加到对应的树中。

    • func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
      	assert1(path[0] == '/', "path must begin with '/'")
      	assert1(method != "", "HTTP method can not be empty")
      	assert1(len(handlers) > 0, "there must be at least one handler")
      
      	debugPrintRoute(method, path, handlers)
      	// 检查有没有对应method集合的路由
      	root := engine.trees.get(method)
      	if root == nil {
      		// 没有 创建一个新的路由节点
      		root = new(node)
      		// 添加该method的路由tree到当前的路由到路由树里
      		engine.trees = append(engine.trees, methodTree{method: method, root: root})
      	}
      	// 添加路由
      	root.addRoute(path, handlers)
      }
      
[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)

为什么这条调试信息的最后,显示的是 3 handlers?明明我们只注册了 /ping 这一条路由而已,是不是应该是一个 Handler。其实不然,我们看看上述创建函数链 HandlersChain 的详细步骤,就知道为什么了,如下:

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
}

combineHandlers 方法中,最终函数链 HandlersChain 的是由 group.Handlers 和外部传入的 handlers 组成的,从拷贝的顺序来看,group.Handlers 的优先级高于外部传入的 handlers

结合 Use 方法来看,很显然是在 gin.Default 方法中注册的中间件影响了这个结果,因为中间件也属于 group.Handlers 的一部分,也就是在调用 gin.Use,就已经注册进去了,如下:

engine.Use(Logger(), Recovery())
...
// engine.Use 会调用 engine.RouterGroup.Use(middleware...)
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

因此所注册的路由加上内部默认设置的两个中间件,最终使得显示的结果为 3 handlers

r. Run

支撑实际运行 HTTP Server 的 Run 方法,

func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	// 执行http包的ListenAndServe方法 启动路由
	// engine实现了http.Handler接口 所以在这里作为参数传参进去
	// 后面我们再看engine.ServeHTTP的具体逻辑
	err = http.ListenAndServe(address, engine)
	return
}
...
⬇️
// 下面就是网络相关了
// 监听IP+端口
ln, err := net.Listen("tcp", addr)
⬇️
// 上面执行完了监听
// 接着就是Serve
srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
⬇️
// Accept请求
rw, e := l.Accept()
⬇️
// 使用goroutine去处理一个请求
// 最终就执行的是engine的ServeHTTP方法
go c.serve(ctx)

该方法会通过解析地址,再调用 http.ListenAndServe 将 Engine 实例作为 Handler 注册进去,然后启动服务,开始对外提供 HTTP 服务。

func ListenAndServe(addr string, handler Handler) error {}
...
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

这里值得关注的是,为什么 Engine 实例能够传进去呢,明明形参要求的是 Handler 接口类型?

  • 在 Go 语言中如果某个结构体实现了 interface 定义声明的那些方法,那么就可以认为这个结构体实现了 interface。
// 接着我们来看看engine的ServeHTTP方法的具体内容
// engine实现http.Handler接口ServeHTTP的具体方法
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// 获取一个上下文实例
	// 从实例池获取性能高
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)	// 重置获取到的上下文实例的http.ResponseWriter
	c.Request = req	// 重置获取到的上下文实例*http.Request
	c.reset()	// 重置获取到的上下文实例的其他属性

	// 实际处理请求的地方
	// 传递当前的上下文
	engine.handleHTTPRequest(c)

	//归还上下文实例
	engine.pool.Put(c)
}
  • sync.Pool 对象池中获取一个上下文对象。
  • 重新初始化取出来的上下文对象。
  • 处理外部的 HTTP 请求。
  • 处理完毕,将取出的上下文对象返回给对象池。

这里的池化不懂,主要是sync.Poold是什么

在这里上下文的池化主要是为了防止频繁反复生成上下文对象,相对的提高性能,并且针对 gin 本身的处理逻辑进行二次封装处理。

中间件

此处需要对“中间件”的概念进行一个说明,因为在分布式系统中也遇到了中间件:是一类提供系统软件和应用软件之间连接、便于软件各部件之间的沟通的软件,应用软件可以借助中间件在不同的技术架构之间共享信息与资源。中间件位于客户机服务器的操作系统之上,管理着计算资源和网络通信。(维基百科)·

评判是否是中间件的关键在于:

  1. 性质:中间件是软件。
  2. 作用层级:系统软件和应用软件之间软件各部件之间;管理客户机与系统软件之间的计算资源和网络通信。
  3. 服务对象:中间件为应用软件服务,应用软件为最终用户服务,最终用户并不直接使用中间件。

也可以理解中间件为:将具体业务和底层逻辑解耦的组件

参考链接:中间件是什么?https://www.zhihu.com/question/19730582/answer/1663627873

posted @ 2022-11-30 22:24  KpHang  阅读(173)  评论(0编辑  收藏  举报