首页 > 其他分享 >Gin框架入门

Gin框架入门

时间:2022-11-30 22:34:00浏览次数:55  
标签:engine HTTP group 入门 框架 handlers 中间件 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

标签:engine,HTTP,group,入门,框架,handlers,中间件,Gin,路由
From: https://www.cnblogs.com/kphang/p/16939980.html

相关文章

  • YouTube汇编入门课
    汇编还是被逼着学习汇编,哭唧唧o(╥﹏╥)o。之前看操作系统的那门课程也用过riscv的汇编,但是都是copyandWrite(抄代码当做写代码,滑稽。寄存器eax是halfraxRegister,这意......
  • 前端入门第一天
    目录前端入门第一天今日内容概要今日内容详细前端与后端的概念前端前戏HTTP协议HTML简介HTML概览预备知识head内常见标签body内基本标签常见符号body内布局标签body内常用......
  • 【arm64】centos7安装nginx_vts_exporter,实现监控
    由于官方nginx_vts_exporter是没有arm架构的包的,最新版本也只有源码包,需要arm安装包或者安装最新版本,只能自己下载源码包进行编译安装 nginx_vts_exporter是用go写的,自......
  • Redis从入门到精通:中级篇
    本文目录上一篇文章以认识Redis为主,写了Redis系列的第一篇,现在开启第二部分的学习,在本文中,我们将看到以下内容:Redis数据结构String、Hash、List、Set、SortedSet及相关操作,......
  • Pytest - 使用pytest-xdsit 插件运行后 logging 模块日志不会输出的问题
    背景:自己写的日志打印模块,用pytest-n=auto后日志就不会输出#tools.set_loggging.pyimportlogging.handlersimportsysfromconcurrent_log_handlerimportConc......
  • go redis v8 gin session
    今天使用到gin的模版功能,于是学习了一下登录session因为gin有自家开发好的sessionredis。所以在redis支持方面。已经有支持好的了但是看了一下golangredis方面,发现有......
  • 极客编程python入门-生成器
    generator通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。在Python中,这种一边循环一边计算的机制,称为生成器:generator。只要把一个列表生......
  • xml_概述和xml_快速入门
    xml_概述:概述:Extensible Markup Language可扩展标记语言可扩展:标签都是自定义的<user> <student>标记语言:标签构成的语言功能存储数据......
  • 大数据学习6之分布式日志收集框架Flume——Flume实战应用之从指定的网络端口采集数据
    从指定的网络端口采集数据输出到控制台进入官网,查看文档,settingupanagent,看到asimpleexample使用Flume的关键就是写flume的agent配置文件1.配置source2.配置channel......
  • 大数据学习4之分布式日志收集框架Flume——背景介绍与架构及核心组件说明
    业务现状分析问题:WebServer/ApplicationServer分散在各个机器上,想用大数据平台Hadoop进行统计分析,日志如何收集到Hadoop平台上?shell脚本cp到hadoop集群的机器上,再通......